c++c++17operator-overloadingcommutativity

When cppreference says that * should be overloaded as a free function for "symmetry", does "symmetry" mean commutativity?


I have implemented a class vec3:

struct vec3 {
    double x = 0, y = 0, z = 0;
    auto& operator*= (double d) {x *= d; y *= d; z *= d; return *this;}
};

auto operator* (const vec3 &a, double d) {auto ret = a; ret *= d; return ret;}

However, expressions of the form vec3{} * 5. compile, while expressions like 5. * vec3{} do not. This confuses me, because cppreference says that

Binary operators are typically implemented as non-members to maintain symmetry (for example, when adding a complex number and an integer, if operator+ is a member function of the complex type, then only complex + integer would compile, and not integer + complex).

Here, I have implemented operator* as a non-member, so shouldn't both vec3{} * 5. and 5. * vec3{} compile, due to the symmetry mentioned in the cppreference quote?


Solution

  • Implicit conversion symmetry of operator overloads

    What cppreference means when it says "symmetry" is symmetry of implicit conversions. When you implement * as a member function, the left hand side is not subject to user-defined conversions. For example,

    This means that vec2 * vec3 and vec3 * vec2 work differently, and such asymmetry is surprising and undesirable. Similarly, the cppreference example complex + integer should work the same as integer + complex, assuming there is an implicit conversion from integer to complex. This would require + to be implemented as:

    complex operator+(complex, complex);
    

    If there was a member function complex::operator+ instead, integer + complex would be impossible.

    Commutative operator overloads

    The "symmetry" you're referring to is commutativity, i.e. the ability to swap the operands and yield the same result for some operation. C++ doesn't make operators commutative by default. It wouldn't be safe to do so for most anyway, especially not for *. Matrix multiplication isn't commutative, but uses the * symbol for example.

    If you want commutativity, you'll have to do it yourself:

    auto operator*(const vec3 &a, double d) { auto ret = a; ret *= d; return ret; }
    auto operator*(double d, const vec3& a) { return a * d; }
    

    Similar to the complex and integer example, you could avoid writing these operator overloads if there was a conversion from double to vec3. However, this conversion is likely undesirable in your example.

    Note that since C++20, != and == are commutative even when overloaded. This is an exception to the rule that you have to define commutativity yourself.