21.1 — Introduction to operator overloading

In lesson 10.10 -- Introduction to function overloading, we discussed function overloading, which provides a mechanism to create identically named functions (so long as each function has a unique function prototype) and resolve function calls to them. This allows you to create variations of a function that can work with different sets of data values, without having to think up a unique name for each variant.

Functions aren’t the only thing in C++ that can be overloaded -- most operators can be overloaded too! We introduced this topic in lesson %Failed lesson reference, id 16842% in order to show how we could overload the I/O operators to input and output enumerated types.

In this chapter, we’ll take a deeper dive into the topic of operator overloading, particularly as it pertains to class types.

A reminder

  • A built-in type is type that is part of the core C++ language. This includes fundamental types (like int and double, as well as some compound types (such as references, pointers, and C-style arrays).
  • A user-defined type is a class type or enumerated type, including those defined in the standard library or by the implementation.
  • A program-defined type is a class type or enumerated type, excluding those defined in the standard library or by the implementation.

Built-in operators and user-defined operators

C++ has two kinds of operators:

  • The C++ language standard defines a bunch of operators that the compiler must implement as part of the core language. These operators, which work with built-in types, are called the built-in operators. The implementation is responsible for

How built-in operators are implemented is left to the compiler, but typically the compiler will just convert a built-in operator (and associated operands) directly into machine language instructions.

  • Operators that work with at least one user-defined type are called user-defined operators. Unlike the built-in operators, user-defined operators are implemented as functions. This enables overloaded operators to piggyback on the function overloading rules.

Key insight

Built-in operators are available as part of the core language, and it is not possible to overload them.

User-defined operators are implemented as functions, and they can be overloaded. They must contain at least one operand of a user-defined type.

Resolving operators in expressions

When evaluating an expression containing an operator, the compiler uses the following rules:

  • If none of the operands are user-defined types, the compiler will match a built-in operator if one exists. If no match can be found, the compiler will error.
  • If any of the operands are user-defined types, the compiler will use the function overload resolution process (described in lesson 10.12 -- Function overload resolution and ambiguous matches) to see if it can find an operator that is an unambiguous best match. This may involve implicitly converting one or more operands to match the parameter types of an overloaded operator. It may also involve implicitly converting user-defined types into fundamental types (via an overloaded typecast, which we’ll cover later in this chapter) so that it can match a built-in operator. If no match can be found (or an ambiguous match is found), the compiler will error.

Consider the following example:

#include <iostream>

int main()
{
    int x { 2 };
    int y { 3 };
    std::cout << x + y << '\n';

    return 0;
}

Because x and y are non-user-defined types, the compiler will determine that this matches to built-in operator+ that takes two int operands. It is up to the compiler to determine how x + y are actually added to produce the result value 5.

Now consider what happens if we try to add two objects of a user-defined type:

#include <iostream>
#include <string>

int main()
{
    std::string string1 { "Hello, " };
    std::string string2 { "World!" };
    std::cout << string1 + string2 << '\n';

    return 0;
}

In this case, because std::string is a user-defined type, the compiler will go through the function overload resolution process to determine whether a best match can be found for an operator+ that can handle two operands of type std::string. The <string> header contains an overloaded operator+ designed for precisely this case. The compiler will thus replace expression string1 + string2 with a function call to this best-match operator+.

Just like with overloads of normal functions, you can define multiple overloaded operator functions with the same name, so long as they are distinct.

Defining an overloaded operator

There are three different ways to define an overload operator:

We’ll cover each in upcoming lessons, and summarize when to use each in lesson %Failed lesson reference, id XX%.

Recapping from lesson %Failed lesson reference, id 16842%, defining an overloaded operator is straightforward:

  • If the operator has a symbolic name, define a function whose name is the word operator and the operator’s symbol(s) (e.g. operator+ or operator<=). Conventionally, no whitespace is placed between these (although the compiler will accept it if you do).
  • If the operator has a word name, define a function whose name is the word operator, a space, and the operator’s name (e.g. operator new).
  • For each operand, add a parameter of the appropriate type (in left-to-right order).
  • Set the return type to whatever type makes sense (see below).
  • Within the body of the overloaded operator function, use a return statement to return the result of the operation.

What should an operator return?

Overloaded operators should return values in the way that is consistent with the original operators.

  • Operators that do not modify their operands (e.g. operator+) should generally return by value.
  • Operators that modify their leftmost operand (e.g. operator=) should generally return the leftmost operand by reference.

What are the limitations on operator overloading?

First, almost any existing operator in C++ can be overloaded. The following operators may not be overloaded:

  • conditional (?:)
  • sizeof
  • scope (::)
  • member selector (.)
  • pointer member selector (.*)
  • typeid
  • the casting operators (e.g. static_cast)
  • noexcept and alignof

Certain operators must be overloaded as non-member or as member functions. We’ll summarize this in lesson %Failed lesson reference, id XX%.

Second, you can only overload the operators that exist. You can not create new operators or rename existing operators. For example, you could not create an operator** to do exponentiation.

Third, the precedence, associativity, and number of operands of the operators cannot be changed.

For example, some new programmers attempt to overload the bitwise XOR operator (^) to do exponentiation. However, in C++, operator^ has a lower precedence level than the basic arithmetic operators, which causes expressions to perform value computation in the wrong order mathematically.

In basic mathematics, exponentiation is resolved before basic arithmetic, so 4 + 3 ^ 2 resolves as 4 + (3 ^ 2) => 4 + 9 => 13.
However, in C++, the arithmetic operators have higher precedence than operator^, so 4 + 3 ^ 2 resolves as (4 + 3) ^ 2 => 7 ^ 2 => 49.

To use such an operator in a compound expression, you’d need to explicitly parenthesize the exponent portion (e.g. 4 + (3 ^ 2)) every time you used it for this to work properly. This isn’t intuitive, and is error prone.

Because of this precedence issue, it’s generally a good idea to use operators only in an analogous way to their original intent.

Best practice

When overloading operators, it’s best to keep the function of the operators as close to the original intent of the operators as possible.

Furthermore, because operators don’t have descriptive names, they should only be used in cases where their behavior is obvious. For example, operator+ is a reasonable choice for a string class to do concatenation of strings, because concatenation is essentially adding two stings together. But what about operator-? What would you expect that to do if given two string operands? It’s unclear.

Best practice

If the meaning of an overloaded operator is not clear and intuitive, use a named function instead.

Fourth, at least one of the operands in an overloaded operator must be a user-defined type. This means you can overload operator+(int, Foo), but not operator+(int, double).

Because standard library classes are considered to be user-defined, this means you could define operator+(double, const std::string&). However, this is not a good idea because a future language standard could define this overload, which could break any programs that used your overload. For this reason, your overloaded operators should operate on at least one program-defined type.

Best practice

All overloaded operators should operate on at least one operand with a program-defined type.

Within those confines, you will still find plenty of value in defining overloaded operators for your program-defined types. For example, you could overload the operator+ to concatenate two objects of your program-defined string class, or add two Fraction objects together. You could overload operator<< to make it easy to print your class to the screen (or a file), or operator== to compare two Point objects.

Because operator overloading allows you to work with your program-defined types using an intuitive and concise syntax, operator overloading is one of the most used features in C++.

guest
Your email address will not be displayed
Find a mistake? Leave a comment above!
Correction-related comments will be deleted after processing to help reduce clutter. Thanks for helping to make the site better for everyone!
Avatars from https://gravatar.com/ are connected to your provided email address.
Notify me about replies:  
77 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments