42

Following the discussion in question Incrementation and decrementation of “enum class”, I'd like to ask about the possible implementation of arithmetic operators for enum class types.

Example from the original question:

enum class Colors { Black, Blue, White, END_OF_LIST };

// Special behavior for ++Colors
Colors& operator++( Colors &c ) {
  c = static_cast<Colors>( static_cast<int>(c) + 1 );
  if ( c == Colors::END_OF_LIST )
    c = Colors::Black;
  return c;
}

Is there a way to implement arithmetic operators without casting to a type with already defined operators? I can't think of any, but casting bothers me. Casts are usually indication of something wrong and there has to be a very good reason for their usage. I would expect the language to allow implementation of an operator to be achievable without forcing to a specific type.

Update Dec 2018: One of the papers towards C++17 seems to address this at least partially by allowing conversions between enum class variable and the underlying type: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0138r2.pdf

HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
SomeWittyUsername
  • 18,025
  • 3
  • 42
  • 85
  • 17
    "Casts are usually indication of something wrong" ... casts are there to be used. And it's sometimes better to explicitly cast than to let the compiler assume whatever it wants. – Yochai Timmer Mar 16 '13 at 16:08
  • @YochaiTimmer on the same note, by using casts I can diminish all the advantages of `enum class` back to the original `enum`. I find this strange - what's the point of introducing a new integral type which is based on numerical value but without a native support for operations on the underlying numerical type. – SomeWittyUsername Mar 16 '13 at 16:11
  • You can use a switch. But this is not scalable... – Synxis Mar 16 '13 at 16:21
  • @Synxis yep, that basically means creating a workaround for the language incompleteness in this aspect. – SomeWittyUsername Mar 16 '13 at 16:24
  • You can see an iterable solution that would work for any enums [here](http://stackoverflow.com/a/15411645/16287). Because of how enums can be defined, there would need to be some memory overhead. – Drew Dormann Mar 16 '13 at 16:26
  • @DrewDormann That's nice, but it's cheating :) It actually uses another construct (`std::set`) to overcome the problems of `enum`. If that's the case, why not work with `std::set` directly instead of `enum`? – SomeWittyUsername Mar 16 '13 at 16:32
  • 8
    @icepack: *You're* the one who doesn't want to do the simple and obvious cast. You're the one who thinks that having to cast in certain limited circumstances is a language deficiency. So you're the one who's going to have to live with the consequences of that. – Nicol Bolas Mar 16 '13 at 16:53
  • 1
    @NicolBolas I disagree with your statement. In many, many cases doing a simple cast will drastically make your life easier, at least for a while. But it will bite off in the future. I don't see a reason to provide an integral construct in the language without an integral set of valid operations on it (or at least a way to implement them naturally without forcing, which is what cast essentially means). Yes, `enum` has its problems but `enum class` solves them by introducing other problems instead – SomeWittyUsername Mar 16 '13 at 16:57
  • 9
    @icepack: "*I don't see a reason to provide an integral construct in the language without an integral set of valid operations on it*" If you wanted those constructs, you shouldn't be using an `enum class`. The *entire purpose* of this is to *not* be implicitly convertible to an integer. It's to *not* treat it as an integer type. Therefore, if you want to perform certain integer operations on it, you must define them *yourself*. – Nicol Bolas Mar 16 '13 at 16:59
  • 4
    @icepack: "*But it will bite off in the future.*" No, it won't; that's dogmatic thinking. You're only doing a cast in certain, specific and *isolated* locations. – Nicol Bolas Mar 16 '13 at 17:01
  • 1
    @NicolBolas I see 2 problems with this. First, if it's not convertible to integer (or double, float etc.), what's the purpose of having the underlying type specified? If it's only to allow explicit casting back to the type, we are back to the original `enum`. If that's the case, using `enum` is simpler. Second, I'm not requesting a conversion to integer type. I'm asking for an iteration (for example) over the range of valid values without addressing or using in any way the underlying integer type. – SomeWittyUsername Mar 16 '13 at 17:07
  • @NicolBolas I'm not anti-cast, I agree there places where it's acceptable, but I don't see a sufficient reasoning in this specific case. – SomeWittyUsername Mar 16 '13 at 17:08
  • 5
    "by using casts I can diminish all the advantages of `enum class` back to the original `enum`" -- no. The advantage of `enum class` is that it does not *implicitly* convert to an integral type, not that it cannot be converted to an integral type. When you want to treat a given `enum class` like an integral type, you are able to do so, and can explicitly do so inside your `enum class` interface. Yes, adding iteration-over-elements and the like support in-language would be a fine thing, but `enum class` was added because it was both useful and easy. – Yakk - Adam Nevraumont Mar 16 '13 at 17:24
  • @Yakk and how would you implement the ++ for non-contiguous `enum class` (even with casts)? Maybe it's just me, but `enum class` definition seems to me like a half-baked thing. It's type-safe but less useful. It effectively resolves the mix of 2 different types (which is very important) but it also removes all the implicitly supported operations of standard enums (except for comparison). Yes, `enum` also doesn't support non-contiguous range iteration, but with `enum class` even the contiguous range iteration is removed. – SomeWittyUsername Mar 16 '13 at 17:40
  • 4
    @icepack You keep on talking as if casting from `enum class` to underlying type was impossible or undesireable. You, personally, do not like it: but that is not the design of `enum class`. In an `enum class`, you can access the underlying type at any point via a cast, and that is part of the design. Access to underlying type operators is not removed, it just no longer happens *implicitly*. And non-contiguous `enum`s? Unless you cast an `enum` to the underlying type, the value of an `enum` means *nothing*. – Yakk - Adam Nevraumont Mar 16 '13 at 17:46
  • 1
    @Yakk That's true. But `enum class` is an abstraction of higher-level language. Fundamental part of any abstraction is to hide the technical details that are not relevant to the client. If I want to represent a set of fruits as {apple, banana, lemon}, I'm not really interested in adding 1 to the underlying implementation of apple to reach the banana. I'm using it to perform operations at the same level as definition of my type. That's why I use abstractions - to avoid dealing with underlying technicalities. But with `enum class` I'm forced to deal with them anyway. I see a contradiction here. – SomeWittyUsername Mar 16 '13 at 18:38
  • 2
    And the user of the `enum class` doesn't have to. But someone implementing `operator++` on an `enum class` is part of the implementation, and should be fiddling around with implementation details. Note that not every `enum class` is limited to the values inside the `{}` -- you can write a bitfield that has overriden `|&^~`, and the algebraic closure are all "valid" elements of the `enum class`. For such a type, the "next `enum`" operation is invalid. `enum class` is just about letting the implementer pick when to treat it as the underlying type, it doesn't block access. – Yakk - Adam Nevraumont Mar 16 '13 at 21:12
  • @Yakk So we see the `enum class` function in different light. I see it as a distinct integral enumeration type. So just like operations on `double` do not require any operation on `int` or any other integral type I expect operations on `enum class` to behave the same in this aspect. – SomeWittyUsername Mar 17 '13 at 12:40

3 Answers3

43

The no-cast solution is to use switch. However, you can generate a pseudo-switch using templates. The principle is to recursively process all values of the enum using a template list (or a parameter pack). So, here are 3 methods I found.

Test enum:

enum class Fruit
{
    apple,
    banana,
    orange,
    pineapple,
    lemon
};

The vanilla switch (live here):

Fruit& operator++(Fruit& f)
{
    switch(f)
    {
        case Fruit::apple:     return f = Fruit::banana;
        case Fruit::banana:    return f = Fruit::orange;
        case Fruit::orange:    return f = Fruit::pineapple;
        case Fruit::pineapple: return f = Fruit::lemon;
        case Fruit::lemon:     return f = Fruit::apple;
    }
}

The C++03-ish method (live here):

template<typename E, E v>
struct EnumValue
{
    static const E value = v;
};

template<typename h, typename t>
struct StaticList
{
    typedef h head;
    typedef t tail;
};

template<typename list, typename first>
struct CyclicHead
{
    typedef typename list::head item;
};

template<typename first>
struct CyclicHead<void,first>
{
    typedef first item;
};

template<typename E, typename list, typename first = typename list::head>
struct Advance
{
    typedef typename list::head lh;
    typedef typename list::tail lt;
    typedef typename CyclicHead<lt, first>::item next;

    static void advance(E& value)
    {
        if(value == lh::value)
            value = next::value;
        else
            Advance<E, typename list::tail, first>::advance(value);
    }
};

template<typename E, typename f>
struct Advance<E,void,f>
{
    static void advance(E& value)
    {
    }
};

/// Scalable way, C++03-ish
typedef StaticList<EnumValue<Fruit,Fruit::apple>,
        StaticList<EnumValue<Fruit,Fruit::banana>,
        StaticList<EnumValue<Fruit,Fruit::orange>,
        StaticList<EnumValue<Fruit,Fruit::pineapple>,
        StaticList<EnumValue<Fruit,Fruit::lemon>,
        void
> > > > > Fruit_values;

Fruit& operator++(Fruit& f)
{
    Advance<Fruit, Fruit_values>::advance(f);
    return f;
}

The C++11-ish method (live here):

template<typename E, E first, E head>
void advanceEnum(E& v)
{
    if(v == head)
        v = first;
}

template<typename E, E first, E head, E next, E... tail>
void advanceEnum(E& v)
{
    if(v == head)
        v = next;
    else
        advanceEnum<E,first,next,tail...>(v);
}

template<typename E, E first, E... values>
struct EnumValues
{
    static void advance(E& v)
    {
        advanceEnum<E, first, first, values...>(v);
    }
};

/// Scalable way, C++11-ish
typedef EnumValues<Fruit,
        Fruit::apple,
        Fruit::banana,
        Fruit::orange,
        Fruit::pineapple,
        Fruit::lemon
> Fruit_values11;

Fruit& operator++(Fruit& f)
{
    Fruit_values11::advance(f);
    return f;
}

(C++11-ish old version)

You may be able to extend by adding some preprocessor to remove the need to repeat the list of values.

Synxis
  • 9,236
  • 2
  • 42
  • 64
  • 1
    Very nice indeed! For sure, need of such stuff doesn't make the life of a developer easier. Sounds natural to me that such an infrastructure was built in the C++11 standard natively. – SomeWittyUsername Mar 17 '13 at 07:57
  • 1
    I like the templates version because you can use them for more than just increment operator : you can add names, etc... The C++03-ish method is for compiler not supporting variadic templates, and can be adapted for simple enums (for C++03-only compilers). Also, I made the C++1-ish method less verbose. – Synxis Mar 17 '13 at 16:48
6

Every operator in C++ on enums can be written without casting to an underlying type, but the result would be ridiculously verbose.

As an example:

size_t index( Colors c ) {
  switch(c) {
    case Colors::Black: return 0;
    case Colors::Blue: return 1;
    case Colors::White: return 2;
  }
}
Color indexd_color( size_t n ) {
  switch(n%3) {
    case 0: return Colors::Black;
    case 1: return Colors::Blue;
    case 2: return Colors::White;
  }
}
Colors increment( Colors c, size_t n = 1 ) {
  return indexed_color( index(c) + n );
}
Colors decrement( Colors c, size_t n = 1 ) {
  return indexed_color( index(c)+3 - (n%3) );
}
Colors& operator++( Colors& c ) {
  c = increment(c)
  return c;
}
Colors operator++( Colors& c, bool ) {
  Colors retval = c;
  c = increment(c)
  return retval;
}

and a smart compiler will be able to turn these into operations that are directly on the base integral type.

But casting to a base integral type in the interface of your enum class is not a bad thing. And operators are part of the interface for your enum class.

If you don't like that loop through size_t and consider it a fake cast, you can just write:

Colors increment( Colors c ) {
  switch(c) {
    case Colors::Black: return Colors::Blue;
    case Colors::Blue: return Colors::White;
    case Colors::White: return Colors::Black;
  }
}

and similarly for decrement, and implement increment-by-n as loops of repeated increment.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • That's the best I can think of as well, but as you said "*it's ridiculously verbose*", and even more important - not scalable. – SomeWittyUsername Mar 16 '13 at 17:31
  • 1
    @SomeWittyUsername: `enum class` was created precisely for the reason to make it impossible (to do accidentally; so it requires a ludicruous amount of hoop-jumping to do on purpose). If this protective mechanism stands in your way, simply don't use `enum class`, just old `enum`. – SF. May 05 '16 at 12:25
  • you can use the following code to cast the enums in a safe way: http://melpon.org/wandbox/permlink/8eJuKbrjem8q8srL – Jan Christoph Uhde Oct 12 '16 at 21:31
  • @jan are you certain? If the underlying type value is 'out of gamut', is it legal to cast to enum? Might be worth a SO question. – Yakk - Adam Nevraumont Oct 12 '16 at 23:45
  • @JanChristophUhde No, I'm worried about gamut. I'm aware that enums guarantee values in the "bit gamut" of their values as valid. I'm uncertain if they guarantee that all values in the underlying type can be converted to values of the enum in a defined way. (I get that the naive implementation of enums seemingly makes this "of course it would work"; I'm asking if you are aware if the standard supports that assumption.) – Yakk - Adam Nevraumont Oct 13 '16 at 14:36
  • http://melpon.org/wandbox/permlink/8eJuKbrjem8q8srL - oh sorry you have been faster than i thought i have updated the example, if something is wrong there will be a warning. and no there is no way to convert a long long into an int, either the value fits or it does not. in the end it is a static_cast – Jan Christoph Uhde Oct 13 '16 at 14:53
  • @JanChristophUhde I am talking about casting from the underlying type to the enum. You appear to not know the answer to the question. I'll have to read the standard. – Yakk - Adam Nevraumont Oct 13 '16 at 14:56
  • expr.static.cast [5.2.9/10] " The value is unchanged if the original value is within the range of the enumeration values (7.2). Otherwise, the behavior is undefined". 7.2/8 [dcl.enum] implies that with a non-fixed underlying type, not all underlying type values are valid enum values, and conversion outside of that range is UB. – Yakk - Adam Nevraumont Oct 13 '16 at 15:04
  • Yes casting a long to an int that is out of range is obviously wrong. Again a) you can check for the types to be equal. b) you are likely to get a warning. – Jan Christoph Uhde Oct 13 '16 at 15:18
  • @JanChristophUhde No, `enum bob{x=1;}; bob x = (bob)std::underlying_type_t(-1);` is, as far as I can tell, undefined behavior under the standard. This has nothing to do with longs or ints. This is casting from the underlying type of an enum with no fixed underlying type to an enum. I cited the standard that seems to state that `bob` has no fixed underlying type (but it does have an underlying type, it is just not called fixed under the standard), and there are different rules for cast-to-enum for a type with no fixed underlying type. – Yakk - Adam Nevraumont Oct 13 '16 at 15:21
  • @JanChristophUhde • the link is dead. – Eljay Aug 31 '22 at 13:30
  • @Eljay it was probably something similar to https://github.com/extcpp/basics/blob/master/include/ext/util/enum.hpp – Jan Christoph Uhde Sep 29 '22 at 14:21
1
enum class Colors { Black, Blue, White };

Colors operator++(Colors& color)
{
    color = (color == Colors::White) ? Colors::Black : Colors(int(color) + 1);
    return color;
}

Check in C++ Shell

vigord
  • 21
  • 3
  • 2
    This starts to cause issues as soon as some enum values have explicit values. See for example http://cpp.sh/6pa7t – moggi Jul 19 '17 at 05:24