Allow out-of-class operator overloading for enum classes

6,459 views
Skip to first unread message

Aarón Bueno Villares

unread,
Nov 20, 2017, 11:59:03 AM11/20/17
to ISO C++ Standard - Future Proposals
Enum classes doesn't allow, by default, implicit conversion, not even to the underlying type:

enum class A : int { zero, one };
void foo(int) {}

int main() { foo(A::zero); }

and that is a good thing, but sometimes, you need a enum class only to avoid name colissions, for example:

enum class table1_cols { id, name };
enum class table2_cols { id, address };

If table1_cols and table2_cols were raw enums, there would be a colission between both id named values, since they have global namespace scope, but your intention is to treat them as raw enums to pass to other functions receiving integers, comparing them, using them on switchs, and so on, but still be forced to qualify its use with enum_type::value syntax for security reasons.

You can of course create a parametrized class for enums with the suitable overloads of comparision operators and conversions operators and constructors, but then you have an extra layer of qualification when dealing with those values (for example, enum_wrapper<table1_cols>(table1_cols::id), or just enum_wrapper(table1_cols::id) in C++17, but I think that is an innecesary extra typing anyway. 

I know that is not hard to just cast it using the C-notation, like (int)table1_cols::id, but in certain situations you have a mix of int, unsigneds, and enums, for example, because you use enums as indexes for your own purposes (and thus you deal with std::size_t's) and interact with third party libraries that uses ints (for example, model indexes), or make temporary arithmetic operations with unsigneds that forces you to cast them to ints, and them comparing them to your enums, and you get a lot of compilers errors if you do the wrong castings when activating all of warning and error compiler flags. Add to the mix templates, autos, and so on. 

So, why not just allow overload conversion operators externally but only for enums classes, while disallowing them for other types?

template<class integral_t>
operator integral_t(table1_cols t)
{
   
static_assert(std::is_integral<integral_t>(), "Only to integrals!!");
   
return static_cast<int>(t);
}



or at least, allow implicit conversions to the underlying type, or add to the language a syntax resource to activate implicit casts to the underlying type (consequently, its user responsability to define which is the most common integral underlying type that you will cast to).

Strong types enums were added for a reason, but sometimes you have a well-defined semantics and a controled-used of your enums that makes the usual risks of using raw enums unnecesary (but you still wants some of the features of strong enums), and I think such a language tool can save you a lot of time.

j c

unread,
Nov 20, 2017, 12:16:57 PM11/20/17
to std-pr...@isocpp.org
I posted here previously about adding a 'std::underlying_cast' for enums.
People agreed with the concept, but not the name (which is fine).

--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+unsubscribe@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/b9b24701-fd4e-411b-91e1-608bba06fe96%40isocpp.org.

Nicol Bolas

unread,
Nov 20, 2017, 12:18:36 PM11/20/17
to ISO C++ Standard - Future Proposals


On Monday, November 20, 2017 at 11:59:03 AM UTC-5, Aarón Bueno Villares wrote:
Enum classes doesn't allow, by default, implicit conversion, not even to the underlying type:

enum class A : int { zero, one };
void foo(int) {}

int main() { foo(A::zero); }

and that is a good thing, but sometimes, you need a enum class only to avoid name colissions, for example:

enum class table1_cols { id, name };
enum class table2_cols { id, address };

If table1_cols and table2_cols were raw enums, there would be a colission between both id named values, since they have global namespace scope,

If they were unscoped enums, you could still refer to them as `table1_cols::id` and `table2_cols::id`. So just do that.

Aarón Bueno Villares

unread,
Nov 20, 2017, 12:36:45 PM11/20/17
to ISO C++ Standard - Future Proposals
It gives you (g++ at least) a compiler error because `id` has been redeclared. 

Aarón Bueno Villares

unread,
Nov 20, 2017, 12:38:42 PM11/20/17
to ISO C++ Standard - Future Proposals
On Monday, 20 November 2017 18:16:57 UTC+1, j c wrote:
I posted here previously about adding a 'std::underlying_cast' for enums.
People agreed with the concept, but not the name (which is fine).

That is not an implicit conversion, which is what I'm talking about.

Bo Persson

unread,
Nov 20, 2017, 1:19:55 PM11/20/17
to std-pr...@isocpp.org
On 2017-11-20 18:36, Aarón Bueno Villares wrote:
>
>
> On Monday, 20 November 2017 18:18:36 UTC+1, Nicol Bolas wrote:
>
>
>
> On Monday, November 20, 2017 at 11:59:03 AM UTC-5, Aarón Bueno
> Villares wrote:
>
> Enum classes doesn't allow, by default, implicit conversion, not
> even to the underlying type:
>
> |
> enumclassA :int{zero,one };
> voidfoo(int){}
>
> intmain(){foo(A::zero);}
> |
>
> and that is a good thing, but sometimes, you need a enum class
> only to avoid name colissions, for example:
>
> |
> enumclasstable1_cols {id,name };
> enumclasstable2_cols {id,address };
> |
>
> If table1_cols and table2_cols were raw enums, there would be a
> colission between both id named values, since they have global
> namespace scope,
>
>
> If they were unscoped enums, you could /still/ refer to them as
> `table1_cols::id` and `table2_cols::id`. So just do that.
>
>
> It gives you (g++ at least) a compiler error because `id` has been
> redeclared.
>

So disable that warning, or put the unscoped enum inside its own scope
(namespace or struct).

namespace table1_cols { enum {id,name}; };




Bo Persson


Nicol Bolas

unread,
Nov 20, 2017, 1:30:53 PM11/20/17
to ISO C++ Standard - Future Proposals, b...@gmb.dk
But then, it's difficult to use them as a typed quantity. That is, making a variable of type `table1_cols` isn't possible, since it's a namespace. You'd have to use `decltype` gymnastics.

Personally, I try to design things so that either we're talking about an integer or we aren't. The OP's problem only arises when you have an enumeration that you sometimes use as an enum and sometimes use as an integer. I don't think we should make language facilities to facilitate that corner case; we should make it as painful as possible to discourage design which leads to that.


Aarón Bueno Villares

unread,
Nov 20, 2017, 1:53:49 PM11/20/17
to ISO C++ Standard - Future Proposals, b...@gmb.dk
By it becomes a problem when dealing with third-party libraries. Sometimes is a bit tricky to adapt third-party libraries to your own designs.

Because of the risks of plain enums and the restrictions of strong-types enums, in my experience, the only comfortable and adecuate use case for using enums is to use them as flags. Any other use of enums have caused troubles of some kind to me.

It would be awesome to make enums more class-alike.

Bo Persson

unread,
Nov 20, 2017, 4:27:30 PM11/20/17
to std-pr...@isocpp.org
This was to solve one problem - implicit conversion to ints.

If you want a type name for this, you can name the enum and use that as
a type

namespace table1 { enum cols {id,name}; };

using table1_cols = table1::cols;


>
> Personally, I try to design things so that either we're talking about an
> integer or we aren't. The OP's problem only arises when you have an
> enumeration that you sometimes use as an enum and sometimes use as an
> integer. I don't think we should make language facilities to facilitate
> that corner case; we should make it as painful as possible to
> /discourage/ design which leads to that.
>

Yes, it is a problem when you use enum class to try to make the type not
implicitly convertible to an integer, except sometimes. It is hard to
write the code for "implicitly convertible only when I want it to be".


Bo Persson



Nicol Bolas

unread,
Nov 20, 2017, 4:50:42 PM11/20/17
to ISO C++ Standard - Future Proposals, b...@gmb.dk
I think the problem isn't that the values aren't implicitly convertible. It's that explicit conversion to the underlying type is a gigantic eyesore. So... let's solve that problem: let's have a way to explicitly convert an enumerator to the underlying type, only without the verbosity of `static_cast`. A simple solution is to add an operator for that. Or rather, use an existing operator for this, since this is totally not worth creating an operator for:

enum class blah {...};

auto operator+(blah b) {return static_cast<std::underlying_type_t<blah>>(b);}

Then, when you want to convert an enumerator into the underlying type, you just use `+`. Granted, `+` may not be the best unary operator to use here, but feel free to use whatever seems natural.

You could even wrap such a definition in a macro, allowing it to be reused for any enumerator type you would like.

j c

unread,
Nov 21, 2017, 3:10:25 AM11/21/17
to std-pr...@isocpp.org, b...@gmb.dk
That won't be very helpful when you want to write an enum value to an ostream (ambiguous overloads)
Want to write an enum to a binary file? You need to know the correct size.

Now, you could add new overloads for every enum you have, but hopefully we can all agree that that would be daft.
The language needs to make this easy


Nicol Bolas

unread,
Nov 21, 2017, 9:52:31 AM11/21/17
to ISO C++ Standard - Future Proposals, b...@gmb.dk
I don't know what you mean by "correct size", since pretty much any definition I could think of for that term would be spelled `std::underlying_type_t<enum_type>`.

And there is exactly one overload of `operator+` here: the one that goes to `std::underlying_type_t`. So I fail to see how there can be "ambiguous overloads" from this code.

Now, you could add new overloads for every enum you have, but hopefully we can all agree that that would be daft.

No, we can't. This is not a problem that crops up so frequently that it needs a solution. The conversion from enum to integer is not supposed to be a common operation, and there are many enumerations where that's simply unnecessary or are decidedly infrequent.

This is a tool for the rarer enumerations where this kind of conversion is frequent. Type-safety should not be so lightly discarded.

Not only that, the OP suggested being able to add something to an enumeration definition to make it implicitly convertible, with one idea being allowing the user to override the conversion operator within it. So it would be something you have to write for each enumeration you want to do this with.

This `operator+` overload is essentially the same idea, only that you have to explicitly invoke it.

The language needs to make this easy

No, it doesn't.

Are there enumerations that are essentially integers-with-names, where you're frequently jumping back and forth between integer and enumeration? Yes. But that's not all or even most of them.

So the language doesn't need to make this easy.

j c

unread,
Nov 21, 2017, 11:58:37 AM11/21/17
to std-pr...@isocpp.org, b...@gmb.dk

--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+unsubscribe@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.


> The conversion from enum to integer is not supposed to be a common operation

According to who? Enums aren't supposed to be printed or serialised? That's news to me.
Why doesn't this code 'just work' when T is an enum?

std::ostream& operator<<(std::ostream& os, const T& value)
{
    os << value;
    return os;
}


> Not only that, the OP suggested being able to add something to an enumeration definition to make it implicitly convertible

What else needs to be added?

enum class E : bool { ... }

The compiler already knows everything it needs, but chooses (at the moment) to pretend it doesn't.


> This is a tool for the rarer enumerations where this kind of conversion is frequent. Type-safety should not be so lightly discarded.

'Rarer' is entirely subjective. Totally agree on the type safety and so nothing should be implicitly converted.
std::underlying_cast (don't worry about the actual name), if adopted, makes it pretty clear to developers what's going on, far more so than an operator would.
Then, the above code could be written as:

std::ostream& operator<<(std::ostream& os, const T& value)
{
    os << std::underlying_cast(value);
    return os;
}

This, in my opinion, would count as the language making something easy, as much as that goes against the ethos of C++.

Nicol Bolas

unread,
Nov 21, 2017, 12:29:30 PM11/21/17
to ISO C++ Standard - Future Proposals, b...@gmb.dk
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.


> The conversion from enum to integer is not supposed to be a common operation

According to who? Enums aren't supposed to be printed or serialised? That's news to me.
Why doesn't this code 'just work' when T is an enum?

Why doesn't this code 'just work' when `T` is `struct T{int i;};`?

It's for the exact same reason: the struct `T` is not an `int`. Just like the enumeration `T` is not an `int`.

> Not only that, the OP suggested being able to add something to an enumeration definition to make it implicitly convertible

What else needs to be added?

The decision to make it implicitly convertible. You seem to forget: the very name of proposal that introduced `enum class` was "Strongly typed enums". The inability to implicitly convert to integer was not just a feature, but one of the very cornerstones of the proposal. Arbitrarily undoing that is basically saying that we don't want strongly typed enums.

And we do.

If you're using a strongly typed enum, and you want it to be implicitly convertible, then you have contradicted yourself. The problem is that strongly typed enum syntax is also bundled with scoped enumerations. That you can't have one without the other.

enum class E : bool { ... }

The compiler already knows everything it needs, but chooses (at the moment) to pretend it doesn't.


> This is a tool for the rarer enumerations where this kind of conversion is frequent. Type-safety should not be so lightly discarded.

'Rarer' is entirely subjective. Totally agree on the type safety and so nothing should be implicitly converted.
std::underlying_cast (don't worry about the actual name), if adopted, makes it pretty clear to developers what's going on, far more so than an operator would.

You're not understanding something here. The OP can already do explicit conversions; they're legal and well-defined. What the OP wants is to not have to type anything to get conversions to happen.

The compromise position between "use a cast" and "implicit conversion" is "use less text than a cast". Whether you spell it `static_cast<int>`, `(int)` or even `underlying_cast`, you had to type a lot to make it work. `+` is one character. I feel that's a reasonable compromise between breaking type safety (by declaring the type as a whole to be implicitly convertible) and the verbosity of invoking that conversion.

Is it less clear? Certainly. But lack of clarity doesn't stop people from using operators to force lambdas to convert to function pointers:

auto lamb = [](...) {...};
auto func_ptr = -lamb;

Why do this? Because it works and it's shorter than doing it manually.

 
Then, the above code could be written as:

std::ostream& operator<<(std::ostream& os, const T& value)
{
    os << std::underlying_cast(value);
    return os;
}

This, in my opinion, would count as the language making something easy, as much as that goes against the ethos of C++.

But that's not the language doing anything. You can implement this `underlying_cast` right now:

template<typename T> requires is_enum_v<T>
auto underlying_cast(T t) {return static_cast<std::underlying_type_t<T>>(t);}
 
Nothing is stopping you from creating that utility function. You can even use SFINAE rather than the `requires` clause.

Aarón Bueno Villares

unread,
Nov 22, 2017, 11:14:23 PM11/22/17
to ISO C++ Standard - Future Proposals, b...@gmb.dk
`+` is one character. I feel that's a reasonable compromise between breaking type safety (by declaring the type as a whole to be implicitly convertible) and the verbosity of invoking that conversion.

I agree. The only thing I originally needed is how to work with enumerators as if they were classes (because I needed some methods to state some properties of each enumerator), with the implicit conversion case as my first concern to go further on, which gave me the most of my headaches.

Overloading `operator+` is short and simple enough, but to overload it generically you do still need strong-typed enums to avoid name conflicts. Now I have everything I need without adding nothing new to the language:

namespace col_enum {

   
template<class enum_t>
   
inline auto operator+(enum_t e) noexcept
   
{
       
static_assert(std::is_enum_v<enum_t>, "Only for enums!!");
       
return std::underlying_type<enum_t>(e);
   
}

   
template<class enum_t>
   
inline std::string title(enum_t e); // Header title / string version of the enum

   
template<class enum_t>
   
inline std::enable_if_t<std::is_enum_v<enum_t>::value, std::ostream&>
   
operator<<(std::ostream& os, enum_t e)
   
{ return os << title(e); }

   
template<class enum_t>
   
inline auto& operator++(enum_t& e) noexcept
   
{ return e = enum_t(+e + 1); }

   
// "Table size"
   
template<class enum_t>
   
constexpr std::size_t size() noexcept;

   
// We assume one is an enumerator and the other
   
// is an integral type, or two enumerators of same type.
   
// For simplicity, no further check is done (it should though).
   
template<class a_t, class b_t>
   
inline bool operator==(a_t const& a, b_t const& b)
   
{
     
using cmp_t = std::common_type_t<decltype(+a), decltype(+b)>;
     
return static_cast<cmp_t>(a) == static_cast<cmp_t>(b);
   
}

   
template<class a_t, class b_t>
   
inline bool operator!=(a_t a, b_t b) { return !(a == b); }

   
// Any other "enumerator-member" or operator, if needed.
}

// User-code
namespace col_enum {
   
enum class table1_col { id, name };

   
template<>
   
constexpr std::size_t size<table1_col>() noexcept { return 2; }

   
template<>
   
inline std::string title<table1_col>(table1_col c)
   
{
     
switch(c) {
     
case table1_col::id:
         
return "ID";
     
case table1_col::name:
         
return "name";
     
default:
         
throw std::logic_error("invalid col");
     
}
   
}
}

namespace col_enum {
   
// Other enumerator for other table,
   
// with their corresponding methods.
}

class my_table1_model
{
public:
 
using col_t = col_enum::table1_col;
 
// ...

 
void fun(int c)
 
{
     
if (c >= col_enum::size<col_t>())
       
throw std::logic_error("Whaaat?");

     
if (c == col_t::id)
        third_party_fun
(this, +col_t::name,
                        boost
::lexical_cast<std::string>(col_t::name));
 
}
};

It works fine, it is comfortable to use and you can add new enumerators trivially. You can also add easly "new members" to that enumerator wrapper namespace, to get class-like enumerator interfaces (which I guess I'm not the first guy to desire that), thanks to ADL, without downsides so far in my use cases.

I can even think of something like "enumerator inheritance" (for adding new members, not for adding enumeration names) by declaring a new namespace for your table_col enumerator, with an inner "using namespace col_enum" within. That way you can add new members which are specific to your new enum plus thouse inherited from `col_enum`, but I haven't test it because I haven't need it.

Aarón Bueno Villares

unread,
Nov 22, 2017, 11:15:29 PM11/22/17
to ISO C++ Standard - Future Proposals
Granted, `+` may not be the best unary operator to use here, but feel free to use whatever seems natural.

I think it is. It doesn't seem the most natural, but it's very short and more or less a familiar trick (I don't know why I didn't think about it before). `boost::hana` uses it, and it's also commonly used with lambdas, so, anybody that sees `+enumerator_name` could at least guess that I'm forcing some kind of conversion and there's an overload somewhere.
Reply all
Reply to author
Forward
0 new messages