Operator Overloading: Common Practice

In my last blog post I have told you about the basics of operator overloading, about the when and how, and which operators can be overloaded. In this post I will go into the details and write a bit about each operator and how a straight forward implementation might look if the operator is meant to work similar to built in operators.

When overloading operators, there are only few restrictions on the signatures and on whether they have to be implemented as methods or free functions, except for the number of parameters. So it would be well possible to define an addition of a Circle and a Rectangle that results in a Pyramid, but certainly nobody would want to use such crazy operators. So usability demands that operators that are meant for “normal” use should have the expected semantics for the objects of your classes.

The operators

I will present the overloadable C++ operators, some in groups and some individually. For each operator or operator family there is a usual semantic, i.e. what an operator is commonly expected to do. Usually that semantic follows the phrase “do as the ints do” or, in some cases, “do as the pointers do”.  In addition to the semantics I will show typical declarations and implementations of the operators as far as they exist, and I will mention any specialities of the operators.

In the code examples, X denotes a user defined type for which the operators are implemented. T is an arbitrary type, it may be user defined or built in. To stick with a common convention I will call parameters of binary operators lhs and rhs, meaning “left hand side” and “right hand side”, respectively. If the operator should be declared as a method of the class, this is indicated by prefixing the declaration with X:: as it would be used in the definition outside of the class definition. (See my last post on why some operators should be implemented as free functions and some as class methods)

`operator=`

  • Right-to-left evaluation: unlike most binary operators, `operator=` is right-associative, i.e. `a = b = c` means `a = (b = c)`.

copy assingment

  • Semantics: assignment `a = b`. The value or state of `b` gets assigned to `a`. In addition, a reference to `a` gets returned. This enables chain assignments like in `c = a = b`.
  • Usual declaration: `X& X::operator= (X const& rhs)`. Other argument types are possible, but not too usual, because if the assignment `x = t` with different types is possible, usually a conversion from `T` to `X` also exists so that `t` can be converted implicitly and the single overload is sufficient.
  • Usual implementation:
    X& X::operator= (X const& rhs) {
      if (this != &rhs) {
        //perform element wise copy, or:
        X tmp(rhs); //copy constructor
        swap(tmp);
      }
      return *this;
    }
    

    The shown implementation that uses the copy constructor and then swaps the content of `*this` with the temporary copy has the advantage of not having to reimplement the copy operations that usually are the same as in the copy constructor. In addition, since swap operations are usually `noexcept` operations, this implementation provides the strong exception guarantee, i.e. the object is not left in a partially changed state if an exception occurs.

Move assignment (since C++11)

  • Semantics: assignment `a = temporary()`. The value or state of the rvalue gets assigned to `a` by moving its contents into `a`. A reference to `a` gets returned.
  • Usual declaration and implementation:
    X& X::operator= (X&& rhs) {
      //take the guts from rhs
      return *this;
    }
    

    Taking the internals of the other object is dependent on the class members. It usually makes only sense if the objects of class `X` own some kind of resource, e.g. memory allocated on the heap or a file, a lock etc.

  • Compiler generated `operator=`: The two shown versions of this operator are the only operators that can be generated by the compiler. If no `operator=` is declared for a class, the compiler tries to generate public copy and move assignment operators if a corresponding assignment is found in the code. Since C++11 the compiler can also be explicitly told to generate them by defaulting them:
    X& X::operator= (X const& rhs) = default;

    The generated copy/move assignment simply calls a copy/move for each single member. Generation fails if one of the members is not copyable/moveable, e.g. if the class has nonstatic constants or references as members, or if the corresponding `operator=` of one of the members is not accessible or can not be generated.

`operator+,-,*,/,%`

  • Semantics: Addition, substraction, multiplication, division, modulo operation for numbers. A new object is returned that contains the resulting value. The following statements are analogous for all five operators:
  • Usual declaration and implementation:
    X operator+ (X const& lhs, X const& rhs) {
      X tmp(lhs);
      tmp += rhs;
      return tmp;
    }

    Usually, if an `operator+` exists, it makes sense to also have `operator+=` to enable the shorter notation `a += b` instead of `a = a + b`. In that case `operator+` should be implemented in terms of `operator+=` like shown above to prevent code duplication. `operator+` should be implemented as a free function to enable implicit conversions of the first argument. If the operator is not implemented in terms of `operator+=`, it therefore has to be either declared as friend of class `X` or relay the execution of the assignment to a public method of the class, e.g.

    X operator+ (X const& lhs, X const& rhs) {
      // create a new object that represents the sum of lhs and rhs:
      return lhs.plus(rhs);
    }

unary `operator+,-`

  • Semantics: Positive, negative (sign). `operator+` usually does nothing and is therefore not very common. `operator-` returns the negative of its argument.
  • Usual declaration and implementation:
    X X::operator- () const {
      return /* a negative copy of *this */;  
    }
    
    X X::operator+ () const {
      return *this;
    }

`operator<<, >>`

  • Semantics: For built in types the shift operators apply a bitwise shifting of the binary representation of the left argument. Overloading the operators with those semantics is rare, the only case I know of is `std::bitset`. However, the streams of the standard library have introduced a new semantic of these operators as input/output operators, and overloading the operators for I/O operations with user defined values and/or IO-objects is very common. A reference to the lhs IO-object is returned in order to enable chaining like `io << a << b << c`.
  • Usual declaration and implementation: Since it is not possible to add methods to the standard library iostream classes, the shift operators for those stream classes and your user defined types have to be overloaded as free functions:
    ostream& operator<< (ostream& os, X const& x) {
      os << /* the formatted data of rhs you want to print */;
      return os;
    }
    
    istream& operator>> (istream& is, X& x) {
      SomeData sd;
      SomeMoreData smd;
      if (is >> sd >> smd) {
        rhs.setSomeData(sd);
        rhs.setSomeMoreData(smd);
      }
      return lhs;
    }

    In addition to iostreams, the lhs type can be any class you want to behave like IO-objects, which means the rhs type can then be a built in type. Since you control the IO class, you can implement the shift operators for basic types as member functions, depending on your needs you can even declare them virtual.

    MyIO& MyIO::operator<< (int rhs) {
      doYourThingWith(rhs);
      return *this;
    }

    IO-operators for your IO-classes together with arbitrary user defined classes that are not closely related to the IO-classes should be made free functions, as they typically are more related to the user defined class than to the IO-class and the SoC principle demands that you don’t clutter your IO-class with maybe hundreds of unrelated functions and dependencies.

binary `operator&, |, ^`

  • Semantics: Bitwise and, or,  xor. As for bit shift operators it is not very common to overload bit logic operators. Again the only case I know of where these operators are overloaded to implement bitwise semantics is `std::bitset`.

`operator+=, -=, *=, /=, %=`

  • Semantics: `a += b` usually has the same meaning as `a = a + b`, but with only one evaluation of the expression `a`. The behavior of the other operators is analogous.
  • Usual declaration and implementation: Since the operation is meant to modify the left operand, implicit type conversions of that operand are not desirable, because the modification would affect the temporary result of the conversion, not the original value. Therefore these operators should be class methods, which also facilitates access to private data members.
    X& X::operator+= (X const& rhs) {
      //apply changes to *this
      return *this;
    }

    While chaining of these kind of operations is not very usual and odd to read (meaning: don’t do this to not confuse your colleagues), it is still common to return the left operand. As written earlier, `operator+` can be implemented in terms of `operator+=` to avoid duplication of the addition code.

`operator&=, |=, ^=, <<=, >>=`

  • Semantics: Analogous to `operator+=`, but for the bit logic operators. These operators are as rarely overloaded as `operator|` etc. `operator<<=` and `operator>>=` are not used as IO-operators, since `operator<<` and `operator>>` already provide the modification of the left argument.

`operator==, !=`

  • Semantics: Test for equality/inequality. What equality actually means for your objects is very dependent on your class and its uses. However keep in mind that the operator means “equal” and not “almost equal”. In addition, users will expect the usual porperties of euqality:
    1. Reflexivity, i.e. `a == a`.
    2. Symmetry, i.e. if `a == b` then `b == a`
    3. Transitivity, i.e. if `a == b` and `b == c`, then `a == c` as well.
  • Usual declaration and implementation:
    bool operator== (X const& lhs, X cosnt& rhs) {
      return /* check for whatever means equality */
    }
    
    bool operator!= (X const& lhs, X const& rhs) {
      return !(lhs == rhs);
    }

    The latter implementation of `operator!=` avoids code duplication and ensures that it is not possible to have two objects that are neither equal nor inequal or both equal and inequal at the same time.

`operator<, <=, >, >=`

  • Semantics: Test on an ordering relationship (less than, greater than etc.). Usually used if there is only one senisble ordering. E.g. it would be meaningless to compare cars with these operators, because it could mean faster or cheaper or more horsepowers or whatever.
  • Usual declaration and implementation:
    bool operator< (X const& lhs, X const& rhs) {
      return /* compare whatever defines the order */
    }
    
    bool operator> (X const& lhs, X const& rhs) {
      return rhs < lhs;
    }
    

    Implementing `operator>` in terms of `operator<` or vice versa ensures that the behavior is as a user would expect. `operator<=` can be implemented in different ways, depending on the nature of the ordering relation. The canonical way would be an implementation in terms of `operator<` and `operator==`. If the order is a total ordering relationship, what isn’t greater must be less or equal, so it can be implemented solely in terms of `operator>`. In addition, for a total ordering relationship, if `a` is neither less nor greater than `b`, `a` must be equal to `b`, so `operator==` can itself be implemented in terms of `operator<` alone:

    bool operator== (X const& lhs, X const& rhs) {
      return !(lhs < rhs) && !(rhs < lhs);
    }

    You might want to look up the terms “partial ordering”, “strict weak ordering” and “total ordering” to get a sense for the different possible ordering relationships.

`operator++, –`

  • Semantics: `a++` (postincrement) increses the value `a` by one and returns the original value. In contrast `++a` (preincrement) returns the new value after the increment. Analogously for the decrement `operator–`.
  • Usual declaration and implementation:
    X& X::operator++() { //preincrement 
      /* somehow increment, e.g. *this += 1*/; 
      return *this; 
    } 
    
    X X::operator++(int) { //postincrement 
      X oldValue(*this); 
      ++(*this); 
      return oldValue; 
    }

    The formal `int` parameter for the postfix operator is only a means to distinguish the two versions, it makes no sense to evaluate it, and the argument cannot be specified. Because of the temporary copy or otherwise necessary calculation of the old value in the postincrement, it is preferred to use the preincrement if the return value is not evaluated, e.g. in `for` loops with iterators.

 `operator()`

  • Semantics: Execution of a function object (functor). Usually not meant to solely make changes to the object itself, but to make it act like a function, maybe with some internal parameters. Function objects are mostly lightweight. A good example for the use of function objects are the comparators and predicates used in the algorithms and associative containers of the standard library.
  • No restrictions on parameters: in contrast to other operators, there are no restrictions to the number and type of parameters. The operator can be overloaded only as a class method.
  • Example declaration:
    Foo X::operator() (Bar br, Baz const& bz);

`operator[]`

  • Semantics: array access, indexed access for containers, e.g. for `std::vector`, `std::map`, `std::array`.
  • Declaration: The parameter type can be chosen freely. The return type often is a reference to whatever is stored inside the container class. Often the operator is overloaded with a const and a non-const version to allow element modification for non-const containers and disallow it for const containers:
    Element_t& X::operator[](Index_t const& index);
    
    const Element_t& X::operator[](Index_t const& index) const;

`operator!`

  • Semantics: Negation. `operator!` implicates a boolean context, unlike the complement `operator~`. Usually if the negation is possible it is expected that the object itself is usable in a boolean context. To enable this, provide an explicit conversion operator to bool. Overloading operator! is not necessary in that case.
  • Usual declaration and implementation:
    bool X::operator!() const {
      return !/*some evaluation of *this*/;
    }

`explicit operator bool`

  • Semantics: Validation, usage in a boolean context. Best known candidates for this kind of conversion are smart pointer classes.
  • Special case: Conversion operators can be defined to virtually any type. However the conversion to bool is special, so it deserves its own section. Since bool is convertible to int and this conversion is not a user defined conversion, enabling an implicit conversion from a type X to bool means, any object of type X can also be implicitly converted to int, giving 0 or 1. Therefore objects of type X could participate in overload resolution in many unexpected cases which can make using X a nightmare. That has been a known problem for a long time, and looking up “safe bool idiom” will give you a lot of information of how not to covert to bool but something that is only convertible to bool. Luckily, C++11 solved the problem by introducing explicit conversion operators and stating that the compiler shall try to explicitly cast objects to bool if they are used in a boolean context, as in `if (x)`.
  • Implementation:
    explicit X::operator bool() const {
      return /* if this is true or false */;
    }

`operator&&, ||`

  • Semantics: Logical and, or. These operators exist for built in types only for boolean arguments and are implemented as short circuit operators. That means that the second argument is not evaluated if the first argument already determines the outcome. If you overload the logical operators for user defined types, the short circuit evaluation will not be used, i.e. both operands will always be evaluated. For that reason it is uncommon to overload these operators, at least for the usual boolean semantics.

unary `operator*`

  • Semantics: Dereferencing pointers. This operator is usually overloaded for smart pointer and iterator classes. Returns a reference to whatever the object points to.
  • Usual declaration and implementation: Smart pointers and iterators often store a raw pointer to whatever they point to. I that case this operator just dereferences that internal pointer:
    T& X::operator*() const {
      return *_ptr;
    }

`operator->`

  • Semantics: Member access through pointer. As `operator*`, this operator is usually overloaded for smart pointer and iterator types. It returns a raw pointer or some other object that has an overloaded `operator->`. If a `->` operator is encountered in the code, the compiler chains calls to `operator->` as long as the results are of user defined types, until the return type is a raw pointer which is then dereferenced via the built in `->`.
  • Usual implementation: Returns the often stored raw pointer:
    T* X::operator->() const { return _ptr; }

`operator->*`

  • Semantics: Pointer-to-member access through pointer. Again an operator for smart pointers and iterators. It takes a pointer-to-member and applies it on whatever `*this` points to, i.e. `objPtr->*memPtr` should be the same as `(*objPtr).*memPtr`. Because it is seldomly used, and because its use can be emulated as shown above, it is only rarely implemented.
  • Possible implementation:
    template <typename T, class V>
    T& X::operator->*(T V::* memptr)
    {
      return (operator*()).*memptr;
    }

    Here `X` is the smart poitner type, `V` the type or a base type of what `X` points to, and `T` the type or a base type of what the pointer-to-member points to. Pretty confusing,a nd no wonder this operator is rarely overloaded.

unary `operator&`

  • Semantics: Addressoperator. There is no “usual” overload, and I have never heard of a usefully overloaded `operator&`. On the contrary, overloading it might break functionality that relies on the operator returning an address and does not use C++11’s `std::address_of` yet.

`operator,`

  • Semantics: The built in comma operator, when applied to two expressions, evaluates both expressions in order and returns the value of the second. It is usually only used in places where only one expression is allowed but the side effects of two expressions are needed, namely in for loop headers, e.g. if more than one loop variable has to be incremented. Since the evaluation order of functions, including overloads of `operator,` is not guaranteed, it is not recommended to overload it.

`operator~`

  • Semantics: Complement operator, one of the rarest operators in C++. Should be expected to return an object of the same type as its argument.

Type conversion operators

  • Semantics: enables implicit or explicit conversions of objects of your class to other types.
  • Declaration:
    //conversion to T, explicit or implicit
    X::operator T() const;  
    
    //explicit conversion to U const&
    explicit X::operator U const&() const; 
    
    //conversion to V&
    V& X::operator V&();

    These declarations look a bit odd, because there is no return type as in normal functions. The return type is part of the operator name and therefore not stated again. It is for the implementer to decide wether implicit conversions should be possible, however too many possible implicit conversions create the risk of unexpected turns the compiler might take during overload resolution. Whether the operator method should be declared `const` or not should be consistent with what the operator returns: returning a new object or a const reference can not change the original object, but returning a non-const reference or pointer to the internals of `X` should be considered a non-const operation.

`operator new, new[], delete, delete[]`

These operators are completely different to all of the above, since they do not work on your user defined objects but control how to accuire memory before your objects get created and discard it after they get destroyed. Overloading these operators is a big topic in itself and therefore is beyond the scope of this post.

Conclusion

The lenght of this post alone shows how many operators there are to overload. However, it is best to stick to known ground and not get too fancy. Don’t overload operators just because you can. Overload them if you feel it is natural and would not be intuitive if they were missing. But then keep in mind that if you overload one operator, there are probably more that a user would expect to be overloaded too. The sometimes tedious work of additional boilerplate operators can be done for you with a library called Boost.Operators that I am going to write about in the future.

 

Previous Post
Next Post

14 Comments


  1. Your X operator+ (X const& lhs, X const& rhs) isn’t wrong, but could be better. The very first thing you do is copying lhs. Taking lhs by copy instead of const-ref enables copy elision and possibly other optimizations. Getting a reference and copy from that reference has no readability advantage either.

    One advantage could be that you can choose which of lhs and rhs to copy. That can have advantages.

    Reply

  2. This was an excellent writeup across all the operators. Will bookmark as good reference. Thank You.

    Reply


  3. Excellent article !

    There is apparently a typographic issue in the implementation sections for the copy and move assignments, : there are several occurences of the html code &amp where an ampersand character would be expected. I don’t think its a browser issue, as the ampersands display correctly in all the other code snippets.

    Reply

    1. Thanks, somehow the plugin seems to have messed up the stored text. Fixed 🙂

      Reply



  4. Very nice overview! Just a few remarks:

    unary

    operator+

    does not obey the rule “do as the ints do”: The result of the built-in operator is a prvalue, you return

    X const&

    . Why?

    There are 3 properties the equality

    operator==

    should satisfy:
    a. Reflexivity: a == a
    b. Symmetry: If a == b the b == a (this is not called commutativity)
    c. Transitivity: If a == b and b == c then a == c

    You claim: “operator (what isn’t greater must be less or equal) or in terms of operator is actually a total order. If it is only a partial order you must use operator< and operator==.

    Reply

    1. Point 3 should read:
      operator (what isn’t greater must be less or equal) or in terms of operator is actually a total order. If it is only a partial order you must use operator< and operator==.

      Reply

    2. Hi Marcel, thanks for the analysis. The original German post was written years ago, before I kenw of C++11, and it made no difference whether operator+ returned a temporary or a reference. I will fix that and the operator== properties.

      I do not completely understand what you mean in point 3, though. In that sentence I just write that operator<= can be written either in terms of operator> alone, or in terms of operator< and operator== together. I did not say anthing about partial or total ordering until after that sentence.

      Reply

      1. Sorry for the confusion (your editor didn’t like my input). My point is, that you cannot implement operator (in the sense you claimed: what isn’t greater must be less or equal). That’s only correct, if operator> induces a total ordering.

        Reply

        1. Thanks Marcel, I’ll clarify that part.

          Reply

  5. No mention of Boost.Operators http://www.boost.org/doc/libs/1_57_0/libs/utility/operators.htm ?

    Boost.Serialization has an interesting overload for binary operator& as a dual contextual <> (output/input).

    I used operator& to return some sort of “logical reference” to the object, possibly this logical reference can be converted to a pointer also for parts of the program that depend on & to return a pointer.

    Reply

    1. Hi alfC, thanks for the comment! As you can see, I did mention Boost.Operators in the last sentence. I will cover it in detail in a future post. I knew of Boost.Serialization and its operator&, but since it is not exactly the “do as the ints do” way to overload it, I did not mention it here.

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *