Operator Overloading: The Basics

Since I do not have much time this week I will translate and update the first of a small series of articles I have written for a German C++ forum some time ago. Operator overloading fits into this blog’s topics in two ways: Operators can be used to make user defined classes act like known types, e.g. like numbers, pointers and iterators. That way they facilitate the usage of those classes. They can also be used to make your objects do whatever you want them to do, for example build structures that save the operations for later evaluation. The latter is especially useful for building embedded DSLs and gives enough subject matter for a whole series of blog posts. This post will cover the former use of operator overloading, i.e. writing operators that behave like “normal” operators.

Introduction

There are operators in many programming languages. It is common to have at least assignment (= or := or similar) and arithmetic operators (+, -, * and /). In most statically typed languages those operators, especially the arithmetic ones, are restricted to built in types. In Java for example, addition with a plus operator + is only possible for intergal and floating point types as well as for strings. If we define our own classes for mathematical objects, e.g. matrices, we can implement a method for their addition, however we can’t call them via the plus operator but have to write something like a = b.add(c).

That constraint does not exist in C++, we can overload almost all known C++ operators. There are many possibilities: we can choose any combination of types for the operands and return types, the only requirement is that at least one operand has a user defined type. So it is not possible to define new operators that take only built in types or overwrite the existing built in operators.

When to overload operators

The general guideline is: Overload operators if and only if it makes sense. Overloading operators makes sense when the operator can be used intuitively and does not provide unpleasant surprises. “Do as the ints do”: Overloaded operators should behave similar to the same operators already provided by the language for built in types. Exceptions prove the rule as always, therefore operators can be overloaded in a different context if the resulting behavior and the correct use are sufficiently documented. A well known example are the shift operators << and >> for the standard library iostream classes which do clearly not behave like the original bit shift operators for integral types.

Here are some good and bad examples for operator overloading: The above mentioned matrix addition is an exemplary case for intuitively overloaded operators. Matrices are mathematical objects, and the addition is a well defined operation, so if implemented correctly the overloaded operator won’t provide any surprises and anyone will know what it means if he encounters code like this:

Matrix a, b;
Matrix c = a + b;

Of course the operator should not be implemented in such a way that the result is the product of the two matrices or something even stranger.

An example for bad operator overloading is the addition of two player objects in a game. What could the designer of the class have in mind with that overload? What would the result be? That question alone shows why overloading the addition for the player class would be unwise: You don’t know what the operation does and that makes it all but unusable. Another, more controversial example is the addition of elements to a container or the addition of two containers. That adding two containers should result in another container may be obvious, but not how the addition happens: For sequential containers it is not obvious if the addition of two sorted containers is itself sorted, for the addition of two std::maps it is not obvious what happens if both operands contain an element with the same key and so on. For that reason such operations are usually implemented with methods that have more descriptive names like append, merge etc. However the library boost::assign provides an operator += that adds one or more elements to a container.

How to overload operators

Overloading operators is like overloading normal functions, where the functions have special names. In fact, when the compilers sees an expression that involves an operator and user defined types, it replaces that expression by a call to the corresponding overloaded operator function. Most of those names start with the keyword operator, followed by the token for the corresponding operator. When the tokens of an operator do not consist of special characters, i.e. for type conversion operators and memory management operators (new, delete etc.) the operator keyword and the operator token have to be separated by whitespace (e.g. operator new), for other operators the whitespace can be omitted (like operator+).

Most operators can be overloaded either as methods of a class or as free functions, but there are a few exceptions which can be overloaded only as class methods. When an overloaded operator is a class method, the first operand is of that class type (it is always *this) and only the second operand has to be declared in the parameter list. In addition, operator methods are not static, except memory management operators.

While overloading an operator as a class method allows direct access to private class members it prevents implicit conversions of the first argument. For that reason binary operators like operator+ are usually overloaded as free functions. Example:

class Rational {
public:
  //Constructor can be used for implicit conversion from int:
  Rational(int numerator, int denominator = 1);     
  Rational operator+(Rational const& rhs) const;
};

int main() {
  Rational a, b, c;
  int i;
  a = b + c; //ok, no conversion necessary
  a = b + i; //ok, implicit conversion of the second argument
  a = i + c; //ERROR: first argument can not be implicitly converted
}

When unary operators get overloaded as free functions they enable an implicit conversion of the argument, which usually is an unexpected feature. On the other hand as pointed out earlier, implicit conversions are often desirable for the first argument of binary operators. This is however not true for binary operators that modify their left argument, i.e. operator+=, operator%=, operator<<= etc., because that modification would then work on the temporary result of the conversion, not on the original value. Therefore the general guideline is as follows:

Implement unary operators and the binary operators of the “X=” family as class methods, all other binary operators as free functions, if possible.

Which operators can be overloaded

We can overload almost all C++ operators with the following exceptions and restrctions:

  • It is not possible to define completely new operators, e.g. an exponential `operator**`
  • The following operators can not be overloaded:
    1. `?:` (ternary conditinonal operator)
    2. `::` (nested name access)
    3. `.` (member access)
    4. `.*` (member access through pointer-to-member)
    5. `sizeof`, `typeid` and the C++ cast operators.
  • The following operators can be overloaded as class methods, but not as free functions:
    1. `=` (assignment)
    2. `->` (member access for pointers)
    3. `()` (function call)
    4. `[]` (indexed access)
    5. `->*` (member access through pointer-to-member for pointers)
    6. conversion operators and class specific operators for memory management.
  • The number of operands, precedence and associativity of all operators is defined by the standard and cannot be changed.
  • At least one operand has to be of a user defined data type. Typedefs to built in types do not count as distinct type.

For most of these operators there are commom implementation practices which I will go into in the next post of this series, so stay tuned!

 

Previous Post
Next Post

6 Comments



  1. As you were talking about which overloads of addition are good and which I bad I wondered:

    You were talking about the addition of two players, which clearly raises the question what the semantic should be, but what if you define the addition as follows: Player operator+(Player const & p; Score_t const & s);
    Would that be okay since the signature shows that you add a specific score to the player.
    I personally would see that as okay because the signature (using the type Score_t) shows that you are adding a score to the player.
    You were talking about the addition of two containers not being okay which also is reasonable. How about adding a single element not with an add but the operator+?
    Here I’d say that isn’t okay either especially if the element type already has the operator + defined (being a numerical type for example).
    Because I wouldn’t know if this operator+ for the container should be a “map(+)” (when thinking in functional programming terms) or an “append”.

    Reply

    1. Hi Markus, thanks for your thoughts!
      With respect to the player-score addition I see a few problems:
      The first is the assignment of players. You usually don’t want to write playerX = playerY because of unclear semantics (would that mean you end up with two players that have the same name, ID, whatever?) – so assignment between players should be made inaccessible. If that is the case, your proposed operator would not be of much use because you can not write playerX = playerX + someScore. The only viable alternative would be to overload operator+= instead to write palyerX += someScore. However, having operator+= but not operator+ feels a bit awkward, and I don’t see much of a benefit compared to a simple addScore method. Then there is the Score_t type. If you want to add a fixed score, then you would either have to write playerX += Score_t(22) if there is no implicit conversion, or playerX += 22 if the conversion exists. The latter case is all but clear for a reader – does that add 22 to a score, health points or gold or some other quantity? The former is not less verbose than playerX.addScore(22), so again I see no benefit there.

      For the container add I agree that such an operator would not be clear. However, in Boost.Assign there are operators defined to add elements to containers. They are interpreted as insertions, not as additions to each element.

      When it comes to operator overloading, there is a grey zone between what makes perfect sense and what may be unclear. For my part, I prefer to stay well away from anything that might be unclear. That leaves me with two kinds of concepts where operator overloading is appropriate: Well known concepts such as numbers, where there is no doubt what the operators mean, and the complete opposite: completely unknown terrain like embedded DSLs, where the user has to learn every aspect from scratch anyways, so the operators can not be misinterpreted at all. For an example of the latter, see this post about embedded DSLs.

      Reply



Leave a Reply

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