Operator Overloading

Introduction

In C++, it is possible to define operators such as + and -> for user-defined types. For example, the <string> header defines a + operator to concatenate strings. This is done by defining an operator function using the operator keyword.

Remarks

The operators for built-in types cannot be changed, operators can only be overloaded for user-defined types. That is, at least one of the operands has to be of a user-defined type.

The following operators cannot be overloaded:

  • The member access or "dot" operator .
  • The pointer to member access operator .*
  • The scope resolution operator, ::
  • The ternary conditional operator, ?:
  • dynamic_cast, static_cast, reinterpret_cast, const_cast, typeid, sizeof, alignof, and noexcept
  • The preprocessing directives, # and ##, which are executed before any type information is available.

There are some operators that you should not (99.98% of the time) overload:

  • && and || (prefer, instead, to use implicit conversion to bool)
  • ,
  • The address-of operator (unary &)

Why? Because they overload operators that another programmer might never expect, resulting in different behavior than anticipated.

For example, the user defined && and || overloads of these operators lose their short-circuit evaluation and lose their special sequencing properties (C++17), the sequencing issue also applies to , operator overloads.

Arithmetic operators

You can overload all basic arithmetic operators:

  • + and +=
  • - and -=
  • * and *=
  • / and /=
  • & and &=
  • | and |=
  • ^ and ^=
  • >> and >>=
  • << and <<=

Overloading for all operators is the same. Scroll down for explanation

Overloading outside of class/struct:

//operator+ should be implemented in terms of operator+=
T operator+(T lhs, const T& rhs)
{
    lhs += rhs;
    return lhs;
}

T& operator+=(T& lhs, const T& rhs)
{
    //Perform addition
    return lhs;
}

Overloading inside of class/struct:

//operator+ should be implemented in terms of operator+=
T operator+(const T& rhs)
{
    *this += rhs;
    return *this;
}

T& operator+=(const T& rhs)
{
    //Perform addition
    return *this;
}

Note: operator+ should return by non-const value, as returning a reference wouldn't make sense (it returns a new object) nor would returning a const value (you should generally not return by const). The first argument is passed by value, why? Because

  1. You can't modify the original object (Object foobar = foo + bar; shouldn't modify foo after all, it wouldn't make sense)
  2. You can't make it const, because you will have to be able to modify the object (because operator+ is implemented in terms of operator+=, which modifies the object)

Passing by const& would be an option, but then you will have to make a temporary copy of the passed object. By passing by value, the compiler does it for you.


operator+= returns a reference to the itself, because it is then possible to chain them (don't use the same variable though, that would be undefined behavior due to sequence points).

The first argument is a reference (we want to modify it), but not const, because then you wouldn't be able to modify it. The second argument should not be modified, and so for performance reason is passed by const& (passing by const reference is faster than by value).

Array subscript operator

You can even overload the array subscript operator [].

You should always (99.98% of the time) implement 2 versions, a const and a not-const version, because if the object is const, it should not be able to modify the object returned by [].

The arguments are passed by const& instead of by value because passing by reference is faster than by value, and const so that the operator doesn't change the index accidentally.

The operators return by reference, because by design you can modify the object [] return, i.e:

std::vector<int> v{ 1 };
v[0] = 2; //Changes value of 1 to 2
          //wouldn't be possible if not returned by reference

You can only overload inside a class/struct:

//I is the index type, normally an int
T& operator[](const I& index)
{
    //Do something
    //return something
}

//I is the index type, normally an int
const T& operator[](const I& index) const
{
    //Do something
    //return something
}

Multiple subscript operators, [][]..., can be achieved via proxy objects. The following example of a simple row-major matrix class demonstrates this:

template<class T>
class matrix {
    // class enabling [][] overload to access matrix elements
    template <class C>
    class proxy_row_vector {
        using reference = decltype(std::declval<C>()[0]);
        using const_reference = decltype(std::declval<C const>()[0]);
    public:
        proxy_row_vector(C& _vec, std::size_t _r_ind, std::size_t _cols)
            : vec(_vec), row_index(_r_ind), cols(_cols) {}
        const_reference operator[](std::size_t _col_index) const {
            return vec[row_index*cols + _col_index];
        }
        reference operator[](std::size_t _col_index) {
            return vec[row_index*cols + _col_index];
        }
    private:
        C& vec;
        std::size_t row_index; // row index to access
        std::size_t cols; // number of columns in matrix
    };

    using const_proxy = proxy_row_vector<const std::vector<T>>;
    using proxy = proxy_row_vector<std::vector<T>>;
public:
    matrix() : mtx(), rows(0), cols(0) {}
    matrix(std::size_t _rows, std::size_t _cols)
        : mtx(_rows*_cols), rows(_rows), cols(_cols) {}

    // call operator[] followed by another [] call to access matrix elements
    const_proxy operator[](std::size_t _row_index) const {
        return const_proxy(mtx, _row_index, cols);
    }

    proxy operator[](std::size_t _row_index) {
        return proxy(mtx, _row_index, cols);
    }
private:
    std::vector<T> mtx;
    std::size_t rows;
    std::size_t cols;
};

Assignment operator

The assignment operator is one of the most important operators because it allows you to change the status of a variable.

If you do not overload the assigment operator for your class/struct, it is automatically generated by the compiler: the automatically-generated assignment operator performs a "memberwise assignment", ie by invoking assignment operators on all members, so that one object is copied to the other, a member at time. The assignment operator should be overloaded when the simple memberwise assignment is not suitable for your class/struct, for example if you need to perform a deep copy of an object.

Overloading the assignment operator = is easy, but you should follow some simple steps.

  1. Test for self-assignment. This check is important for two reasons:
    • a self-assignment is a needless copy, so it does not make sense to perform it;
    • the next step will not work in the case of a self-assignment.
  2. Clean the old data. The old data must be replaced with new ones. Now, you can understand the second reason of the previous step: if the content of the object was destroyed, a self-assignment will fail to perform the copy.
  3. Copy all members. If you overload the assigment operator for your class or your struct, it is not automatically generated by the compiler, so you will need to take charge of copying all members from the other object.
  4. Return *this. The operator returns by itself by reference, because it allows chaining (i.e. int b = (a = 6) + 4; //b == 10).
//T is some type
T& operator=(const T& other)
{
    //Do something (like copying values)
    return *this;
}

Note: other is passed by const&, because the object being assigned should not be changed, and passing by reference is faster than by value, and to make sure than operator= doesn't modify it accidentally, it is const.

The assignment operator can only to be overloaded in the class/struct, because the left value of = is always the class/struct itself. Defining it as a free function doesn't have this guarantee, and is disallowed because of that.

When you declare it in the class/struct, the left value is implicitly the class/struct itself, so there is no problem with that.

Bit shift operators for I/O

The operators << and >> are commonly used as "write" and "read" operators:

  • std::ostream overloads << to write variables to the underlying stream (example: std::cout)
  • std::istream overloads >> to read from the underlying stream to a variable (example: std::cin)

The way they do this is similar if you wanted to overload them "normally" outside of the class/struct, except that specifying the arguments are not of the same type:

  • Return type is the stream you want to overload from (for example, std::ostream) passed by reference, to allow chaining (Chaining: std::cout << a << b;). Example: std::ostream&
  • lhs would be the same as the return type
  • rhs is the type you want to allow overloading from (i.e. T), passed by const& instead of value for performance reason (rhs shouldn't be changed anyway). Example: const Vector&.

Example:

//Overload std::ostream operator<< to allow output from Vector's
std::ostream& operator<<(std::ostream& lhs, const Vector& rhs)
{
    lhs << "x: " << rhs.x << " y: " << rhs.y << " z: " << rhs.z << '\n';
    return lhs;
}

Vector v = { 1, 2, 3};

//Now you can do
std::cout << v;

Bitwise NOT operator

Overloading the bitwise NOT (~) is fairly simple. Scroll down for explanation

Overloading outside of class/struct:

T operator~(T lhs)
{
    //Do operation
    return lhs;
}

Overloading inside of class/struct:

T operator~()
{
    T t(*this);
    //Do operation
    return t;
}

Note: operator~ returns by value, because it has to return a new value (the modified value), and not a reference to the value (it would be a reference to the temporary object, which would have garbage value in it as soon as the operator is done). Not const either because the calling code should be able to modify it afterwards (i.e. int a = ~a + 1; should be possible).

Inside the class/struct you have to make a temporary object, because you can't modify this, as it would modify the original object, which shouldn't be the case.

Comparison operators

You can overload all comparison operators:

  • == and !=
  • > and <
  • >= and <=

The recommended way to overload all those operators is by implementing only 2 operators (== and <) and then using those to define the rest. Scroll down for explanation

Overloading outside of class/struct:

//Only implement those 2
bool operator==(const T& lhs, const T& rhs) { /* Compare */ }
bool operator<(const T& lhs, const T& rhs) { /* Compare */ }

//Now you can define the rest
bool operator!=(const T& lhs, const T& rhs) { return !(lhs == rhs); }
bool operator>(const T& lhs, const T& rhs) { return rhs < lhs; }
bool operator<=(const T& lhs, const T& rhs) { return !(lhs > rhs); }
bool operator>=(const T& lhs, const T& rhs) { return !(lhs < rhs); }

Overloading inside of class/struct:

//Note that the functions are const, because if they are not const, you wouldn't be able
//to call them if the object is const

//Only implement those 2
bool operator==(const T& rhs) const { /* Compare */ }
bool operator<(const T& rhs) const { /* Compare */ }

//Now you can define the rest
bool operator!=(const T& rhs) const { return !(*this == rhs); }
bool operator>(const T& rhs) const { return rhs < *this; }
bool operator<=(const T& rhs) const { return !(*this > rhs); }
bool operator>=(const T& rhs) const { return !(*this < rhs); }

The operators obviously return a bool, indicating true or false for the corresponding operation.

All of the operators take their arguments by const&, because the only thing that does operators do is compare, so they shouldn't modify the objects. Passing by & (reference) is faster than by value, and to make sure that the operators don't modify it, it is a const-reference.

Note that the operators inside the class/struct are defined as const, the reason for this is that without the functions being const, comparing const objects would not be possible, as the compiler doesn't know that the operators don't modify anything.

Complex Numbers Revisited

The code below implements a very simple complex number type for which the underlying field is automatically promoted, following the language's type promotion rules, under application of the four basic operators (+, -, *, and /) with a member of a different field (be it another complex<T> or some scalar type).

This is intended to be a holistic example covering operator overloading alongside basic use of templates.

#include <type_traits>

namespace not_std{

using std::decay_t;

//----------------------------------------------------------------
// complex< value_t >
//----------------------------------------------------------------

template<typename value_t>
struct complex
{
    value_t x;
    value_t y;

    complex &operator += (const value_t &x)
    {
        this->x += x;
        return *this;
    }
    complex &operator += (const complex &other)
    {
        this->x += other.x;
        this->y += other.y;
        return *this;
    }

    complex &operator -= (const value_t &x)
    {
        this->x -= x;
        return *this;
    }
    complex &operator -= (const complex &other)
    {
        this->x -= other.x;
        this->y -= other.y;
        return *this;
    }

    complex &operator *= (const value_t &s)
    {
        this->x *= s;
        this->y *= s;
        return *this;
    }
    complex &operator *= (const complex &other)
    {
        (*this) = (*this) * other;
        return *this;
    }

    complex &operator /= (const value_t &s)
    {
        this->x /= s;
        this->y /= s;
        return *this;
    }
    complex &operator /= (const complex &other)
    {
        (*this) = (*this) / other;
        return *this;
    }

    complex(const value_t &x, const value_t &y)
    : x{x}
    , y{y}
    {}

    template<typename other_value_t>
    explicit complex(const complex<other_value_t> &other)
    : x{static_cast<const value_t &>(other.x)}
    , y{static_cast<const value_t &>(other.y)}
    {}

    complex &operator = (const complex &) = default;
    complex &operator = (complex &&) = default;
    complex(const complex &) = default;
    complex(complex &&) = default;
    complex() = default;
};

// Absolute value squared
template<typename value_t>
value_t absqr(const complex<value_t> &z)
{ return z.x*z.x + z.y*z.y; }

//----------------------------------------------------------------
// operator - (negation)
//----------------------------------------------------------------

template<typename value_t>
complex<value_t> operator - (const complex<value_t> &z)
{ return {-z.x, -z.y}; }

//----------------------------------------------------------------
// operator +
//----------------------------------------------------------------

template<typename left_t,typename right_t>
auto operator + (const complex<left_t> &a, const complex<right_t> &b)
-> complex<decay_t<decltype(a.x + b.x)>>
{ return{a.x + b.x, a.y + b.y}; }

template<typename left_t,typename right_t>
auto operator + (const left_t &a, const complex<right_t> &b)
-> complex<decay_t<decltype(a + b.x)>>
{ return{a + b.x, b.y}; }

template<typename left_t,typename right_t>
auto operator + (const complex<left_t> &a, const right_t &b)
-> complex<decay_t<decltype(a.x + b)>>
{ return{a.x + b, a.y}; }

//----------------------------------------------------------------
// operator -
//----------------------------------------------------------------

template<typename left_t,typename right_t>
auto operator - (const complex<left_t> &a, const complex<right_t> &b)
-> complex<decay_t<decltype(a.x - b.x)>>
{ return{a.x - b.x, a.y - b.y}; }

template<typename left_t,typename right_t>
auto operator - (const left_t &a, const complex<right_t> &b)
-> complex<decay_t<decltype(a - b.x)>>
{ return{a - b.x, - b.y}; }

template<typename left_t,typename right_t>
auto operator - (const complex<left_t> &a, const right_t &b)
-> complex<decay_t<decltype(a.x - b)>>
{ return{a.x - b, a.y}; }

//----------------------------------------------------------------
// operator *
//----------------------------------------------------------------

template<typename left_t, typename right_t>
auto operator * (const complex<left_t> &a, const complex<right_t> &b)
-> complex<decay_t<decltype(a.x * b.x)>>
{
    return {
        a.x*b.x - a.y*b.y,
        a.x*b.y + a.y*b.x
        };
}

template<typename left_t, typename right_t>
auto operator * (const left_t &a, const complex<right_t> &b)
-> complex<decay_t<decltype(a * b.x)>>
{ return {a * b.x, a * b.y}; }

template<typename left_t, typename right_t>
auto operator * (const complex<left_t> &a, const right_t &b)
-> complex<decay_t<decltype(a.x * b)>>
{ return {a.x * b, a.y * b}; }

//----------------------------------------------------------------
// operator /
//----------------------------------------------------------------

template<typename left_t, typename right_t>
auto operator / (const complex<left_t> &a, const complex<right_t> &b)
-> complex<decay_t<decltype(a.x / b.x)>>
{
    const auto r = absqr(b);
    return {
        ( a.x*b.x + a.y*b.y) / r,
        (-a.x*b.y + a.y*b.x) / r
        };
}

template<typename left_t, typename right_t>
auto operator / (const left_t &a, const complex<right_t> &b)
-> complex<decay_t<decltype(a / b.x)>>
{
    const auto s = a/absqr(b);
    return {
         b.x * s,
        -b.y * s
        };
}

template<typename left_t, typename right_t>
auto operator / (const complex<left_t> &a, const right_t &b)
-> complex<decay_t<decltype(a.x / b)>>
{ return {a.x / b, a.y / b}; }

}// namespace not_std


int main(int argc, char **argv)
{
    using namespace not_std;

    complex<float> fz{4.0f, 1.0f};

    // makes a complex<double>
    auto dz = fz * 1.0;

    // still a complex<double>
    auto idz = 1.0f/dz;

    // also a complex<double>
    auto one = dz * idz;

    // a complex<double> again
    auto one_again = fz * idz;

    // Operator tests, just to make sure everything compiles.

    complex<float> a{1.0f, -2.0f};
    complex<double> b{3.0, -4.0};

    // All of these are complex<double>
    auto c0 = a + b;
    auto c1 = a - b;
    auto c2 = a * b;
    auto c3 = a / b;

    // All of these are complex<float>
    auto d0 = a + 1;
    auto d1 = 1 + a;
    auto d2 = a - 1;
    auto d3 = 1 - a;
    auto d4 = a * 1;
    auto d5 = 1 * a;
    auto d6 = a / 1;
    auto d7 = 1 / a;

    // All of these are complex<double>
    auto e0 = b + 1;
    auto e1 = 1 + b;
    auto e2 = b - 1;
    auto e3 = 1 - b;
    auto e4 = b * 1;
    auto e5 = 1 * b;
    auto e6 = b / 1;
    auto e7 = 1 / b;

    return 0;
}

Conversion operators

You can overload type operators, so that your type can be implicitly converted into the specified type.

The conversion operator must be defined in a class/struct:

operator T() const { /* return something */ }

Note: the operator is const to allow const objects to be converted.

Example:

struct Text
{
    std::string text;

    // Now Text can be implicitly converted into a const char*
    /*explicit*/ operator const char*() const { return text.data(); }
    // ^^^^^^^
    // to disable implicit conversion
};

Text t;
t.text = "Hello world!";

//Ok
const char* copyoftext = t;

Function call operator

You can overload the function call operator ():

Overloading must be done inside of a class/struct:

//R -> Return type
//Types -> any different type
R operator()(Type name, Type2 name2, ...)
{
    //Do something
    //return something
}

//Use it like this (R is return type, a and b are variables)
R foo = object(a, b, ...);

For example:

struct Sum
{
    int operator()(int a, int b)
    {
        return a + b;
    }
};

//Create instance of struct
Sum sum;
int result = sum(1, 1); //result == 2

Named operators

You can extend C++ with named operators that are "quoted" by standard C++ operators.

First we start with a dozen-line library:

namespace named_operator {
  template<class D>struct make_operator{constexpr make_operator(){}};

  template<class T, char, class O> struct half_apply { T&& lhs; };

  template<class Lhs, class Op>
  half_apply<Lhs, '*', Op> operator*( Lhs&& lhs, make_operator<Op> ) {
    return {std::forward<Lhs>(lhs)};
  }

  template<class Lhs, class Op, class Rhs>
  auto operator*( half_apply<Lhs, '*', Op>&& lhs, Rhs&& rhs )
  -> decltype( named_invoke( std::forward<Lhs>(lhs.lhs), Op{}, std::forward<Rhs>(rhs) ) )
  {
    return named_invoke( std::forward<Lhs>(lhs.lhs), Op{}, std::forward<Rhs>(rhs) );
  }
}

this doesn't do anything yet.

First, appending vectors

namespace my_ns {
  struct append_t : named_operator::make_operator<append_t> {};
  constexpr append_t append{};
  
  template<class T, class A0, class A1>
  std::vector<T, A0> named_invoke( std::vector<T, A0> lhs, append_t, std::vector<T, A1> const& rhs ) {
      lhs.insert( lhs.end(), rhs.begin(), rhs.end() );
      return std::move(lhs);
  }
}
using my_ns::append;

std::vector<int> a {1,2,3};
std::vector<int> b {4,5,6};

auto c = a *append* b;

The core here is that we define an append object of type append_t:named_operator::make_operator<append_t>.

We then overload named_invoke( lhs, append_t, rhs ) for the types we want on the right and left.

The library overloads lhs*append_t, returning a temporary half_apply object. It also overloads half_apply*rhs to call named_invoke( lhs, append_t, rhs ).

We simply have to create the proper append_t token and do an ADL-friendly named_invoke of the proper signature, and everything hooks up and works.

For a more complex example, suppose you want to have element-wise multiplication of elements of a std::array:

template<class=void, std::size_t...Is>
auto indexer( std::index_sequence<Is...> ) {
  return [](auto&& f) {
    return f( std::integral_constant<std::size_t, Is>{}... );
  };
}
template<std::size_t N>
auto indexer() { return indexer( std::make_index_sequence<N>{} ); }

namespace my_ns {
  struct e_times_t : named_operator::make_operator<e_times_t> {};
  constexpr e_times_t e_times{};

  template<class L, class R, std::size_t N,
    class Out=std::decay_t<decltype( std::declval<L const&>()*std::declval<R const&>() )>
  >
  std::array<Out, N> named_invoke( std::array<L, N> const& lhs, e_times_t, std::array<R, N> const& rhs ) {
    using result_type = std::array<Out, N>;
    auto index_over_N = indexer<N>();
    return index_over_N([&](auto...is)->result_type {
      return {{
        (lhs[is] * rhs[is])...
      }};
    });
  }
}

live example.

This element-wise array code can be extended to work on tuples or pairs or C-style arrays, or even variable length containers if you decide what to do if the lengths don't match.

You could also an element-wise operator type and get lhs *element_wise<'+'>* rhs.

Writing a *dot* and *cross* product operators are also obvious uses.

The use of * can be extended to support other delimiters, like +. The delimeter precidence determines the precidence of the named operator, which may be important when translating physics equations over to C++ with minimal use of extra ()s.

With a slight change in the library above, we can support ->*then* operators and extend std::function prior to the standard being updated, or write monadic ->*bind*. It could also have a stateful named operator, where we carefully pass the Op down to the final invoke function, permitting:

named_operator<'*'> append = [](auto lhs, auto&& rhs) {
  using std::begin; using std::end;
  lhs.insert( end(lhs), begin(rhs), end(rhs) );
  return std::move(lhs);
};

generating a named container-appending operator in C++17.

Unary operators

You can overload the 2 unary operators:

  • ++foo and foo++
  • --foo and foo--

Overloading is the same for both types (++ and --). Scroll down for explanation

Overloading outside of class/struct:

//Prefix operator ++foo
T& operator++(T& lhs)
{
    //Perform addition
    return lhs;
}

//Postfix operator foo++ (int argument is used to separate pre- and postfix) 
//Should be implemented in terms of ++foo (prefix operator)
T operator++(T& lhs, int)
{
    T t(lhs);
    ++lhs;
    return t;
}

Overloading inside of class/struct:

//Prefix operator ++foo
T& operator++()
{
    //Perform addition
    return *this;
}

//Postfix operator foo++ (int argument is used to separate pre- and postfix) 
//Should be implemented in terms of ++foo (prefix operator)
T operator++(int)
{
    T t(*this);
    ++(*this);
    return t;
}

Note: The prefix operator returns a reference to itself, so that you can continue operations on it. The first argument is a reference, as the prefix operator changes the object, that's also the reason why it isn't const (you wouldn't be able to modify it otherwise).


The postfix operator returns by value a temporary (the previous value), and so it cannot be a reference, as it would be a reference to a temporary, which would be garbage value at the end of the function, because the temporary variable goes out of scope). It also cannot be const, because you should be able to modify it directly.

The first argument is a non-const reference to the "calling" object, because if it were const, you wouldn't be able to modify it, and if it weren't a reference, you wouldn't change the original value.

It is because of the copying needed in postfix operator overloads that it's better to make it a habit to use prefix ++ instead of postfix ++ in for loops. From the for loop perspective, they're usually functionally equivalent, but there might be a slight performance advantage to using prefix ++, especially with "fat" classes with a lot of members to copy. Example of using prefix ++ in a for loop:

for (list<string>::const_iterator it = tokens.begin();
     it != tokens.end();
     ++it) { // Don't use it++
    ...
}


2016-04-20
2017-07-25
C++ Pedia
Icon