Overloading Operators

A guide on how we can empower our custom types with their own operators, allowing them to be interacted with in a more expressive way
This lesson is part of the course:

Making Games with SDL

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

03a.jpg
Ryan McCombe
Ryan McCombe
Posted

This lesson is a quick introductory tour of operator overloading within C++. It is not intended for those who are entirely new to programming. Rather, the people who may find it useful include:

  • those who have completed our introductory course, but want a quick review
  • those who are already familiar with programming in another language, but new to C++
  • those who have used C++ in the past, but would benefit from a refresher

It summarises several lessons from our introductory course. Anyone looking for more thorough explanations or additional context should consider Chapter 8 of that course.

Previous Course

Intro to Programming with C++

Starting from the fundamentals, become a C++ software engineer, step by step.

Screenshot from Cyberpunk 2077
Screenshot from The Witcher 3: Wild Hunt

We can overload operators like + and *= for our custom types. Operators are implemented in the same way as functions, with a specific naming convention.

For example, if we had a custom type called Vector, and we wanted to give Vectors the ability to be added to other Vectors using the + operator, we would define a function called operator+ as follows:

Vector operator+(const Vector& a,
                 const Vector& b){
  return Vector{
    a.x + b.x, a.y + b.y, a.z + b.z};
}
Vector a { 1.f, 2.f, 3.f };
Vector b { 1.f, 1.f, 1.f };

// { 2.f, 3.f, 4.f }
Vector Sum { a + b };

Operators can also be implemented as part of the struct or class that defines our type:

struct Vector {
  float x;
  float y;
  float z;

  Vector operator*(float Multiplier){
    return Vector{
      x * Multiplier,
      y * Multiplier,
      z * Multiplier
    };
  }
};
Vector a { 1.f, 2.f, 3.f };

// { 2.f, 4.f, 6.f }
Vector Result { a * 2.f };

Unary and Binary Operators

Multiple operators can have the same function name. For example, operator- can be a binary operation, ie, it operates on two objects. This would normally be used to represent arithmetic subtraction (eg VectorA - VectorB)

But, - can also be a unary operator, acting upon only one object. This would normally be used for negation (eg, -VectorA)

The compiler can infer whether we are overloading a unary or binary operator from the number of parameters.

  • When defined outside the class, a binary operator will have two parameters. The left operand will be the first parameter; the right operand will be the second parameter.
  • When defined within the class, a binary operator will have one parameter. The left operand will be the current object; the right operand will be the parameter.
  • When defined outside the class, a unary operator will have one parameter. That parameter will be the operand.
  • When defined within the class, a unary operator will have no parameters. The current object will be the operand.
#include <iostream>

struct Vector {
  float x;
  float y;
  float z;

  Vector operator-(){
    std::cout << "Unary - \n";
    return Vector{-x, -y, -z};
  }

  Vector operator-(const Vector& Other){
    std::cout << "Binary - \n";
    return Vector{
      x - Other.x,
      y - Other.y,
      z - Other.z
    };
  }
};

Vector operator+(const Vector& a){
  std::cout << "Unary + \n";
  return a;
}

Vector operator+(const Vector& a,
                 const Vector& b){
  std::cout << "Binary + \n";
  return Vector{
    a.x + b.x, a.y + b.y, a.z + b.z};
}

int main(){
  Vector A;
  -A;
  A - A;
  +A;
  A + A;
}
Unary - 
Binary - 
Unary + 
Binary +

The this Pointer

For many operators, such as ++ and *=, it is expected that the operator will return a reference to the object it operated on. This is important for allowing operators to be chained on our object, in expressions such as (MyObject *= 2) *= 3. With statements like this, we want subsequent operators to act upon the original object, not a copy of it.

Our previous examples didn’t allow this. However, within a class or struct function, the this keyword returns a pointer to the current object - that is, the object that the function or operator was called upon. Therefore, we can use this to ensure our operators return the exact same object they were used on:

#include <iostream>

struct Vector {
  float x;
  float y;
  float z;

  Vector& operator*=(float Multiplier){
    x *= Multiplier;
    y *= Multiplier;
    z *= Multiplier;
    return *this;
  }
};

int main(){
  Vector A{1.f, 1.f, 1.f};
  (A *= 2) *= 3;
  std::cout << A.x;
}
x component: 6

Postfix and Prefix Operators

Operators like -- and ++ can be used either before or after their operand, and each usage has different behavior.

For example, ++MyInteger would increment the integer and return it. MyInteger++ would increment the integer but return a different integer, which has the value of our original integer before it was incremented.

For our custom types, overloading the prefix ++ works as expected:

struct Vector {
  float x;
  float y;
  float z;

  // ++MyVector (Prefix Operator)
  Vector& operator++(){
    ++x;
    ++y;
    ++z;
    return *this;
  }
};

Overloading the postfix operator has a slightly inelegant syntax. To let the compiler distinguish between which function handles the prefix operator and which handles the postfix, we add an extra unused int parameter to the postfix handler:

#include <iostream>

struct Vector {
  float x;
  float y;
  float z;

  // ++MyVector (Prefix Operator)
  Vector& operator++(){
    std::cout << "Prefix ++ \n";
    ++x;
    ++y;
    ++z;
    return *this;
  }

  // MyVector++ (Postfix Operator)
  Vector operator++(int){
    std::cout << "Postfix ++ \n";
    Vector Original{x, y, z};
    ++x;
    ++y;
    ++z;
    return Original;
  }
};

int main(){
  Vector A;
  ++A;
  A++;
}
Prefix ++ 
Postfix ++

More Object Capabilities

Later in this course, we cover a range of further abilities we can add to our custom types. This includes how we can:

  • Overload the function call operator, () to let our custom types act like functions (functors).
  • Implement iterators to let our custom objects work like arrays and other collections, including compatibility with standardized algorithms and native syntax such as range-based for loops.
  • Set up user-defined conversions, to let our objects be implicitly or explicitly converted to other types, including built-in C++ types
  • Implement three-way comparisons and default comparisons using a single function, thereby allowing our objects to be sorted and compared using boolean operators, such as <= and !=
  • Customize copy and move semantics, for control over how our objects react to low-level operations like being passed to a function, or copied to a new variable

Was this lesson useful?

Edit History

  • — First Published

Ryan McCombe
Ryan McCombe
Posted
This lesson is part of the course:

Making Games with SDL

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Whirlwind Tour of C++ Basics
  • 25.Making Minesweeper with C++ and SDL2
  • 26.Project Setup
  • 27.GPUs and Rasterization
  • 28.SDL Renderers
DreamShaper_v7_cyberpunk_woman_playing_video_games_modest_clot_0.jpg
This lesson is part of the course:

Making Games with SDL

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, unlimited access!

This course includes:

  • 24 Lessons
  • 100+ Code Samples
  • 91% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Polymorphism

An introduction to polymorphism works in C++, and how we can set up our custom types to take advantage of it
02a.jpg
Contact|Privacy Policy|Terms of Use
Copyright © 2023 - All Rights Reserved