Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Please consider adding a default formatter for scoped enumerations #2704

Closed
gh-andre opened this issue Jan 7, 2022 · 12 comments
Closed

Please consider adding a default formatter for scoped enumerations #2704

gh-andre opened this issue Jan 7, 2022 · 12 comments
Labels

Comments

@gh-andre
Copy link

gh-andre commented Jan 7, 2022

I'm switching from fmt v6 and all of a sudden all format calls taking scoped enum values fail and it would take many specialized formatters to handle them in the same way.

Please consider adding a generic enum formatter along these lines, so it just works out of the box:

template <typename E>
struct fmt::formatter<E, std::enable_if_t<std::is_enum_v<E>, char>> :
        formatter<std::underlying_type_t<E>> {
    template <typename FormatContext>
    auto format(const E& value, FormatContext& ctx) -> decltype(ctx.out())
    {
        return fmt::formatter<std::underlying_type_t<E>>::format(static_cast<std::underlying_type_t<E>>(value), ctx);
    }
};

Thanks!

@vitaut
Copy link
Contributor

vitaut commented Jan 7, 2022

Scoped enums are not formattable as int by default intentionally (#1424) and we cannot provide a generic formatter specialization. You can provide your own specializations inheriting from formatter for the underlying type such as int.

@vitaut vitaut closed this as completed Jan 7, 2022
@vitaut
Copy link
Contributor

vitaut commented Jan 7, 2022

However, we could provide a generic formatter under a different name e.g. enum_formatter to simplify user-defined specializations. A PR to add it would be welcome.

@gh-andre
Copy link
Author

gh-andre commented Jan 7, 2022

@vitaut The int in the cast is a typo. The whole example is about enum's underlying type, which would be a reasonable assumption for many cases where there's no need to convert them to named values, like colors.

If you are following the list of enabled formatter specialization list, wouldn't this also include unscoped enumerations, seeing how enabled specializations only include arithmetic types? Unscoped enumerations appear to work without a formatter specialization now.

@gh-andre
Copy link
Author

gh-andre commented Jan 7, 2022

@vitaut Sorry, didn't see the other comment. Will wait for your thoughts on unscoped enumerations before responding.

@vitaut
Copy link
Contributor

vitaut commented Jan 8, 2022

Unscoped enumerations are implicitly convertible to the underlying type so they are formatted as the underlying type. This is consistent with usual C++ rules, e.g. (https://godbolt.org/z/5qb11f86G)

enum E {};
enum class EC {};

void print(int) {}

int main() {
  print(E()); // OK
  print(EC()); // error
}

@gh-andre
Copy link
Author

gh-andre commented Jan 8, 2022

@vitaut I was thinking that because enumerations aren't explicitly covered in the list of enabled formatter specializations, they wouldn't be allowed, but I see your point that since arithmetic types are explicitly allowed, integral promotions can run on unscoped enumerations. Thanks for the insights.

Based on this point, I don't think it's wise to supply a specialization I mentioned in the library, given that when people switch to C++20, it won't be there. I will fix the int typo in my original post, so people can use it as a possible fix.

@vitaut
Copy link
Contributor

vitaut commented Feb 5, 2022

{fmt} now support a simplified API (format_as) for making scoped enums formattable as underlying type, e.g.

#include <fmt/format.h>

namespace kevin_namespacey {
enum class film { house_of_cards, american_beauty, se7en = 7};
auto format_as(film f) { return fmt::underlying(f); }
}

int main() {
  fmt::print("{}", kevin_namespacey::film::se7en);
}

https://godbolt.org/z/4r5945v6P

@gh-andre
Copy link
Author

gh-andre commented Feb 5, 2022

@vitaut Thanks for taking the time to work on a solution.

A couple of thoughts come to mind. Not meant as a criticism - just something to consider.

Seems that a function with a predefined name format_as would need to be defined in every namespace that defines scoped enumerations. This might be quite verbose for projects with multiple namespaces, even if the same utility template function confined to a specific namespace can be reused.

#include <fmt/format.h>

namespace util::fmt {
template<typename T, typename std::enable_if_t<std::is_enum_v<T>, int> = 0>
typename std::underlying_type_t<T> format_as(T e)
{
    return static_cast<typename std::underlying_type_t<T>>(e);
}
}

using util::fmt::format_as;

namespace unit1 {
enum class count_t { one, two, three};
using util::fmt::format_as;
}

namespace unit2 {
enum class count_t {one, three, two};
using util::fmt::format_as;
}

enum class count_t { three, two, one};

int main()
{
    auto print_three = []() -> void
    {
        enum class count_t {zero, one, two, three};
        fmt::print("{}\n", count_t::three);
    };

    print_three();

    fmt::print("{}\n", unit1::count_t::three);
    fmt::print("{}\n", unit2::count_t::three);
    fmt::print("{}\n", count_t::three);

    return 0;
}

Another thought is that this solution cannot be easily detached from {fmt} to use it on its own and when people switch to C++20, all code relying on format_as would be broken.

@vitaut
Copy link
Contributor

vitaut commented Feb 5, 2022

Thanks for the suggestion. Having a reusable format_as is an interesting idea, my only concern is that it can be easily abused by injecting into the global namespace.

@gh-andre
Copy link
Author

gh-andre commented Feb 5, 2022

Agreed. This is also what's most likely will happen in most projects. Having said that, I'm not sure what a good solution would be - I simply don't know the project well enough to offer a good suggestion. Maybe std::is_scoped_enum can be used in some way to provide the default conversion, but the wrinkle here is that it's C++23 and std::format is C++20, so they don't align well.

@chengfzy
Copy link

I have the same problem when using fmt 8.1.1, whilc 8.0.1 is ok. And I don't think this modification in fmt 8.1.1 is convenient

@vitaut
Copy link
Contributor

vitaut commented Feb 19, 2022

As suggested by @gh-andre I added a generic fmt::enums::format_as so you can now make all enums in a namespace formattable as underlying types:

#include <fmt/format.h>

namespace kevin_namespacey {
enum class film { house_of_cards, american_beauty, se7en = 7};
using fmt::enums::format_as;
}

int main() {
  fmt::print("{}", kevin_namespacey::film::se7en);
}

I think it's a reasonable compromise between usability and avoiding undesirable conversions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants