Operator Overloading

Discover operator overloading, allowing us to define custom behavior for operators when used with our custom types
This lesson is part of the course:

Game Dev with SDL2

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

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

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 are 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. Those looking for more thorough explanations or additional context should consider completing 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

Operator Overloading

As we’ve seen, operators such as + and *= can act upon data types built into the C++ language, such as int. However, we can make our custom types interact with these operators too. This is referred to as operator overloading.

Operators are simply functions. For a function to act as an operator, we need to follow a specific naming convention.

For example, imagine we have a custom type called Vector:

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

We wanted to give Vector objects the ability to be added to other Vectors using the + operator, That operator would return a new Vector. To implement this, we’d define a function as follows:

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

Objects of our Vector type can now be added to other objects of that same type, using the binary + operator. And, given our function’s return type is Vector, that operator would return another Vector object:

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. Below, we give our Vector objects the ability to be multiplied with a float using the binary * operator:

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.

Here are some examples:

#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 << "x component: " << 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 a 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

Summary

Operator overloading allows you to define custom behavior for operators when used with objects of your custom types.

By overloading operators, you can make your custom types more intuitive and easier to use. Key takeaways:

  • Operators can be overloaded as member functions or non-member functions
  • The number of parameters determines whether an operator is unary or binary
  • The this pointer can be used to return a reference to the current object
  • Postfix operators have a dummy int parameter to distinguish them from prefix operators
  • Operator overloading enables custom types to support various operations and capabilities

Was this lesson useful?

Next Lesson

Run-time Polymorphism

Learn how to write flexible and extensible C++ code using polymorphism, virtual functions, and dynamic casting
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
Lesson Contents

Operator Overloading

Discover operator overloading, allowing us to define custom behavior for operators when used with our custom types

sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

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

Free, Unlimited Access
  • 53.GPUs and Rasterization
  • 54.SDL Renderers
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

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

Free, unlimited access

This course includes:

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

Run-time Polymorphism

Learn how to write flexible and extensible C++ code using polymorphism, virtual functions, and dynamic casting
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved