0

Normally, when overloading operators, the arithmetic operators (including bitwise operators) are defined in terms of the associated compound assignment operators (e.g., operator+ and operator| typically rely on operator+= and operator|=).

class SomeClass {
    SomeType data;
    // ...

  public:
    SomeClass& operator+=(const SomeClass& other) {
        data += other.data;
        return *this;
    }
    friend SomeClass operator+(SomeClass l, const SomeClass& r) { return l += r; }

    friend SomeClass& operator|=(SomeClass& l, SomeClass r) {
        l.data |= r.data;
        return l;
    }
    friend SomeClass operator|(SomeClass l, SomeClass r) { return l |= r; }
};

However, when it comes to enums (whether unscoped or enum class), the logic is typically reversed, with the preferred solution often being to instead define compound assignment operators in terms of the arithmetic operators1.

using Underlying = uint8_t;

enum class SomeEnum : Underlying {
    A_VALUE   = 0b0000'0001,
    B_VALUE   = 0b0000'0010,
    // ...
    H_VALUE   = 0b1000'0000,

    CLEAR_ALL = 0,
    SET_ALL   = static_cast<uint8_t>(-1),
};

// Arithmetic first.
constexpr SomeEnum operator|(SomeEnum l, SomeEnum r) {
    return static_cast<SomeEnum>(static_cast<Underlying>(l) | static_cast<Underlying>(r));
}
constexpr SomeEnum& operator|=(SomeEnum& l, SomeEnum r) { return l = l | r; }

// Compound assignment first.
constexpr SomeEnum operator+=(SomeEnum& l, SomeEnum r) {
    Underlying ul = static_cast<Underlying>(l);
    ul += static_cast<Underlying>(r);
    return l = static_cast<SomeEnum>(ul);
}
constexpr SomeEnum operator+(SomeEnum l, SomeEnum r) { return l += r; }

1: While the question's answers provide examples of both defining operator|= in terms of operator| and defining operator| in terms of operator|=, with both answers being posted within half an hour of each other (and thus having nearly identical exposure), the former has significantly more upvotes. This suggests that it's the more preferred solution by far.


Now, I can see the reasoning for this approach: The code is noticeably cleaner if we flip things around and define the arithmetic operator first, especially if we also desire compile-time availability. However, it also goes against the pattern used nearly(?) everywhere else, which makes it seem a bit suspect IMO (mainly because it violates the principle of least astonishment).

In light of that, I can't help but wonder: Should we flip things around here? Is the benefit of simplicity worth breaking the unspoken guarantee that "a += b is, in general, more efficient than a + b and should be preferred if possible"2?

Put simply, if defining operators3 for an enumeration, should we define compound operators in terms of the associated arithmetic operators instead of the other way around, as seems to be the common recommendation?

2: See footnote #3 on first link.

3: Typically bitwise operators, but I wanted to speak generally.

1 Answers1

1

Speaking about efficiency is wrong here. Any good compiler would compile them to the same thing. gcc does at -O1, and I would suspect most compilers would too.

Because optimizing would end up with the same thing:

Underlying ul = static_cast<Underlying>(l);
ul += static_cast<Underlying>(r);
return l = static_cast<SomeEnum>(ul);

Underlying ul;
(ul = static_cast<Underlying>(l)) += static_cast<Underlying>(r);
return l = static_cast<SomeEnum>(ul);

Underlying ul;
return l = static_cast<SomeEnum>((ul = static_cast<Underlying>(l)) += static_cast<Underlying>(r));

return l = static_cast<SomeEnum>(static_cast<Underlying>(l) + static_cast<Underlying>(r));

return l = operator+(l, r);

It doesn't make sense to implement operator@ for enums in terms of operator@= because you copy it to its underlying type, do the operation, and then copy it back and assign to the copy. In a class, there should (theoretically) be less copies/moves when using A @= B instead of A = A @ B.

Artyer
  • 31,034
  • 3
  • 47
  • 75
  • That's true, I know they'll typically be the same either way once optimised. I was looking more at programmer expectations, and whether the increased simplicity of switching it around was worth the potential confusion from enums being basically the one exception to the _de facto_ rule. I feel I might've communicated that somewhat poorly, though, my apologies. – Justin Time - Reinstate Monica Sep 29 '19 at 23:42