Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

5.4 Overloading

In C++, function overloading means defining multiple functions with the same name but with different parameter lists (number, types, or order of parameters). The compiler determines which version to call based on the arguments provided at the call site. Overloading allows functions to perform similar tasks on different types of data, improving readability and reducing the need for different function names. For example, an overloaded print() function could handle both int and string arguments, letting you call print(5) or print("Hello") with the same intuitive function name.

Rules for overloading (within a class):

  • Multiple methods in the same class share the same name (add() in this case) but differ in their parameter list (number, types, or order).
  • The return type alone cannot distinguish overloaded methods.
  • The compiler decides which version to call at compile time.

For example:

#include<iostream>
#include<string>
using namespace std;
class myMaths {
public:
virtual int add(int a, int b);
virtual float add(float a, float b);
virtual string add(string a, string b);
};
int myMaths::add(int a, int b) {
return (a+b);
}
float myMaths::add(float a, float b) {
return (a+b);
}
string myMaths::add(string a, string b) {
return (a+b);
}
int main() {
myMaths m;
cout << "1 + 2 = " << m.add(1,2) << "\n";
cout << "1.5 + 2.0 = " << m.add(1.5f,2.0f) << "\n";
cout << "one + two = " << m.add(string("one"), string("two")) << "\n";
return 0;
}

This code gives the output:

1 + 2 = 3
1.5 + 2.0 = 3.5
one + two = onetwo

Note: Methods cannot be distinguished by return type alone; the parameter list must differ (in type or number of arguments).

It is also important to recognise that this example is somewhat misleading. Each add() method in the implementation contains exactly the same statement: return (a + b);

So how does the + operator manage to work with int, float, and even string values? The answer lies in operator overloading—the ability of C++ to provide different behaviours for the same operator depending on the types of its operands.

Quiz
Select 0/1

Which of the following is a strict requirement when overloading functions within the same C++ class?

C++ allows programmers to associate the standard predefined operators (such as +, -, =, ==, etc.) with specific functionality for their own classes. A common example is the std::string class, where the + operator performs concatenation and the = operator performs assignment.

Similarly, we can overload predefined operators to make them meaningful for user-defined classes. For instance, in the Account class we might overload the + operator to allow two Account objects to be summed appropriately. For example in the operatorOverloading.cpp example:

// Operator Overloaded Account Example
#include<iostream>
#include<string>
using namespace std;
class Account{
protected:
int accountNumber;
float balance;
string owner;
public:
Account(string owner, float aBalance, int anAccountNumber);
Account(float aBalance, int anAccountNumber);
Account(int anAccountNumber);
Account operator + (Account);
bool operator == (Account);
virtual void display();
virtual void makeLodgement(float);
virtual void makeWithdrawal(float);
};
Account::Account(string anOwner, float aBalance, int anAccNumber):
accountNumber(anAccNumber), balance(aBalance),
owner (anOwner) {}
Account::Account(float aBalance, int anAccNumber) :
accountNumber(anAccNumber), balance(aBalance),
owner ("Not Defined") {}
Account::Account(int anAccNumber):
accountNumber(anAccNumber), balance(0.0f),
owner ("Not Defined") {}
Account Account::operator + (Account a){
return Account(owner, balance + a.balance, accountNumber);
}
bool Account::operator == (Account a) {
if ((a.balance == balance) && (a.owner == owner) &&
(a.accountNumber == accountNumber))
{ return true; }
else return false;
}
void Account::display() {
cout << "account number: " << accountNumber
<< " has balance: " << balance << " Euro" << "\n";
cout << "This account is owned by: " << owner << "\n";
}
void Account::makeLodgement(float amount) {
balance = balance + amount;
}
void Account::makeWithdrawal(float amount) {
balance = balance - amount;
}
int main() {
Account a = Account(10.50, 123456);
a.display();
Account b = Account("Derek Molloy", 35.50, 123457);
b.display();
Account c = Account("Derek Molloy", 35.50, 123457);
if (b == c) { cout << "b and c are equal!" << "\n"; }
Account d = b + a;
d.display();
return 0;
}

The full source code for this example is in OperatorOverloadAccount.cpp. In this case, the + operator has been overloaded to add the balance of the object on the right-hand side (RHS) of the operator to the balance of the object on the left-hand side (LHS). Other attributes, such as accountNumber or owner, are not added, as this would not make sense in the context of accounts.

The overloaded method creates a new Account object and returns it. This returned object is then assigned to the variable on the LHS of the assignment when the statement: Account d = b + a; is executed. The output from this code is:

account number: 123456 has balance: 10.5 Euro
This account is owned by: Not Defined
account number: 123457 has balance: 35.5 Euro
This account is owned by: Derek Molloy
b and c are equal!
account number: 123457 has balance: 46 Euro
This account is owned by: Derek Molloy

In the line of code: Account d = b + a; we can think of this as happening in two stages:

Stage 1 – The operator call The expression b + a invokes the overloaded + operator. Conceptually, you can imagine this as if the operator were replaced with an add() method, i.e., b.add(a). In fact, you could even think of it as b.+(a). In this example, rather than directly modifying b, the operator creates and returns a new Account object.

  • The first element (b) is the object the operator is called on.
  • The second element (a) is passed as the parameter.

Stage 2 – The assignment The result of b + a (a new Account object) is then assigned to d by the statement:

d = (b + a);

Because the operator implementation is written inside the Account class, it has full access to the protected members of both objects, allowing it to read their balances (and other states if required).

In this example, the == operator is also overloaded, enabling comparison between two Account objects. The specific states compared are up to the programmer’s design. For instance, two accounts might be considered equal if they have the same owner and balance, or the comparison might also include the accountNumber. The == operator can therefore have a class-specific definition of equality.

Overloading the Assignment Operator (Advanced) The assignment operator = (aka the copy assignment operator) is slightly more complicated, because you need to be able to chain operations (e.g., a = b = c), which means that the operator must return a reference to the value assigned. The code is in the file operatorOverloadingAssignment.cpp.

#include <iostream>
using namespace std;
class Number{
private:
int a;
public:
Number(int a);
Number & operator = (const Number & source);
void display();
};
Number::Number(int a): a(a) {}
void Number::display(){
cout << "The value of the object at address " << this << " is " << a << "\n";
}
Number & Number::operator = (const Number & source){
if (this!= &source){ // make sure we are not copying from itself
a = source.a;
}
return *this; // return the reference (value of pointer this)
}
int main(){
Number x(5), y(4), z(3);
x.display();
y.display();
z.display();
cout << "Performing the assignment operator now! " << "\n";
z = y = x;
x.display();
y.display();
z.display();
return 0;
}

Results in the following output:

The value of the object 0x62ff0c is 5
The value of the object 0x62ff08 is 4
The value of the object 0x62ff04 is 3
Performing the assignment operator now!
The value of the object 0x62ff0c is 5
The value of the object 0x62ff08 is 5
The value of the object 0x62ff04 is 5

Non-Member Operators (Advanced)
Operator overloading is powerful and flexible, but it can also introduce complexity. For example, using the Number class introduced earlier, you can overload the + operator to support the following expression (where n is an object of type Number):

n = n + 5;

This is straightforward: you would write an overloaded + operator that accepts an int and adds it to the value stored in the object n. In this case, the integer 5 is passed as an argument to the operator.

However, the reverse expression introduces a complication:

n = 5 + n;

Here, the compiler first attempts to call the + operator on the integer type (int), expecting it to accept a Number object as the right-hand side. Since such an operator does not exist for int, this expression fails to compile.

The solution is to define a non-member operator function, which allows the addition to work regardless of the order of operands (operatorOverloadNonMember.cpp):

#include <iostream>
using namespace std;
class Number {
private:
int a;
public:
Number(int a);
void display();
int getValue() const { return a; } // getter does not modify object
};
Number::Number(int a) : a(a) {}
void Number::display() {
cout << "The value of the object at address " << this << " is " << a << "\n";
}
// Non-member operator overload to support both x + y and y + x.
// Note: this works because we provided a getter method.
// (Friend functions could also be used for direct access.)
Number operator+(const Number& left, const Number& right) {
return Number(left.getValue() + right.getValue());
}
int main() {
Number x(5);
x = 3 + x;
x = x + 2;
x.display();
return 0;
}

This gives the output:

The value of the object at address 0x62ff04 is 10

Overloading the Pre- and Post-Increment Operators (Advanced)
An unusual case of operator overloading worth discussing is the pre-increment (++x) and post-increment (x++) operators. Although they use the same symbol (++), their behaviour differs depending on whether the operator appears before or after the variable. The same principles apply to the pre-decrement (--x) and post-decrement (x--) operators.

The following example shows how these operators can be implemented in C++ for a simple Number class. While the class itself is minimal, the technique can be applied to more complex classes (operatorOverloadIncrement.cpp).

#include <iostream>
using namespace std;
class Number {
private:
int a;
public:
Number(int a);
Number& operator++(); // pre-increment (++x)
Number operator++(int); // post-increment (x++), 'int' is a dummy parameter
void display();
};
Number::Number(int a) : a(a) {}
// Pre-increment: increments before returning the object
Number& Number::operator++() {
a++;
return *this;
}
// Post-increment: increments after returning the original value
Number Number::operator++(int) {
Number temp = *this; // make a copy of the current object
a++;
return temp;
}
void Number::display() {
cout << "The value of the object at address " << this
<< " is " << a << "\n";
}
int main() {
Number x(5), y(0), z(0);
y = x++; // post-increment
cout << "Displaying y = x++ :" << "\n";
y.display();
z = ++x; // pre-increment
cout << "Displaying z = ++x :" << "\n";
z.display();
cout << "Displaying x after both operations :" << "\n";
x.display();
return 0;
}

Important notes:

  • The dummy int parameter in the post-increment operator is a convention used to distinguish it from the pre-increment version. It is always of type int, regardless of the class.
  • The post-increment operator is more computationally expensive than pre-increment, as it must create and return a copy of the object.

Example Output:

Displaying y = x++ :
The value of the object at address 0x62ff08 is 5
Displaying z = ++x :
The value of the object at address 0x62ff04 is 7
Displaying x after both operations :
The value of the object at address 0x62ff0c is 7
Code Cloze
C++

Overloading the Addition Operator

Quiz
Select 0/1

Why does the overloaded copy assignment operator (operator=) typically return a reference to the current object (*this)?

Quiz
Select 0/1

Why is an overloaded post-increment operator (x++) generally more computationally expensive than pre-increment (++x)?

A constructor can take parameters to initialise the state of an object. However, there may be different ways to determine the initial state, depending on the information available.

For example, when creating an account, you might want to:

  • Specify the account number, owner’s name, and balance.
  • Specify the account number and owner’s name, with the balance defaulting to 0.0.
  • Specify the account number, owner’s name, initial balance, and a referee.
  • Or other combinations, depending on the application.

To support these variations, constructors in C++ can be overloaded.

Example: Basic Bank Account with Multiple Constructors (AccountMultipleConstructors.cpp)

#include <iostream>
#include <string>
using namespace std;
class Account {
protected:
int accountNumber;
float balance;
string owner;
public:
Account(string owner, float aBalance, int anAccountNumber);
Account(float aBalance, int anAccountNumber);
Account(int anAccountNumber);
virtual void display();
virtual void makeLodgement(float);
virtual void makeWithdrawal(float);
};
// Constructor with all parameters
Account::Account(string anOwner, float aBalance, int anAccNumber) :
accountNumber(anAccNumber), balance(aBalance),
owner(anOwner) {}
// Constructor with account number and balance only
Account::Account(float aBalance, int anAccNumber) :
accountNumber(anAccNumber), balance(aBalance),
owner("Not Defined") {}
// Constructor with account number only
Account::Account(int anAccNumber) :
accountNumber(anAccNumber), balance(0.0f),
owner("Not Defined") {}
void Account::display() {
cout << "Account number: " << accountNumber
<< " has balance: " << balance << " Euro" << "\n";
cout << "This account is owned by: " << owner << "\n";
}
void Account::makeLodgement(float amount) {
balance += amount;
}
void Account::makeWithdrawal(float amount) {
balance -= amount;
}
int main() {
Account a = Account(10.50, 123456);
a.display();
Account b = Account("Derek Molloy", 35.50, 123457);
b.display();
}

Example Output:

Account number: 123456 has balance: 10.5 Euro
This account is owned by: Not Defined
Account number: 123457 has balance: 35.5 Euro
This account is owned by: Derek Molloy

As shown above, the Account object a is created using the constructor that does not specify an owner. The display() method then outputs "Not Defined" as the owner.

Notes on Constructors and Destructors

  • A destructor cannot be overloaded: there can only be one destructor, since destructors do not take any parameters. \
  • In C++, constructors and destructors are not inherited by derived classes. For example, if a base class defines a constructor with three parameters, the derived class will not automatically have this constructor unless it is explicitly defined.

Reusing Initialisation Logic
A common pattern in C++ is to use a private helper function to avoid repeating initialisation code.

class A {
int x;
void init(int a) { x = a; /* other setup */ }
public:
A(int x) { init(x); }
A() { init(402); }
};

In C++11 and later, this can be simplified using constructor delegation:

class A {
int x;
public:
A(int x) : x(x) { /* other setup */ }
A() : A(402) {} // delegate to the other constructor
};

Implicit and Explicit Constructors (Advanced)
It is possible to make a constructor private or to mark it as explicit to control how it is used. For example, making the default constructor private prevents it from being called by the programmer directly, while explicit prevents unintended implicit conversions.

#include <iostream>
using namespace std;
class X{
private:
int a;
X(); // made private to prevent default constructor being called
public:
X(int a);
void display();
};
X::X(int a): a(a) {}
void X::display(){
cout << "The value of a is " << a << "\n";
}
int main(){
X x(5); // cannot use X x;
x.display();
// Since there is no default constructor and therefore only
// one constructor C++ permits implicit conversion e.g.,
X y = 'A';
y.display();
return 0;
}

This might allow you to require the programmer to use one particular constructor, but it also allows implicit conversions to be performed on construction. For example the line to create the object y automatically converts the character 'A' to the number 65 (its ASCII equivalent) giving the output below:

The value of a is 5
The value of a is 65

If you want to prevent such implicit conversions from being performed, you can use the keyword explicit that then imposes that a variable of the correct type is required. For example, if you change the constructor declaration above to the following:

class X{
private:
...
public:
explicit X(int a);
...
};

Then an error will arise, preventing the implicit conversion from being compiled:

error: conversion from 'char' to non-scalar type 'X' requested
Quiz
Select 0/1

What is the primary purpose of marking a single-parameter constructor with the 'explicit' keyword?

The copy constructor is a special constructor that exists implicitly in every C++ class. Its purpose is to create a new object as a copy of an existing object. By default, the compiler provides a copy constructor that performs a member-wise (shallow) copy: all data members of the source object are duplicated into the new object.

 :::note  This is considered to be a shallow copy because a pointer to a string (or any other equivalent) is copied from the source to the destination, so both objects end up with string pointers that point at the same string in memory. This can be challenging for memory management. If available, a deep copy would create a completely independent copy of the original object that would copy the actual strings to new memory locations. A deep copy is more costly in computational cost and memory, but it is much easier to manage as the destruction of one object has no effect on the other.** :::

The default copy constructor can be invoked automatically when an object is initialised from another object of the same type. For example:

int main() {
Account a = Account("Derek Molloy", 35.00, 34234324);
Account b(a); // calls the default copy constructor
a.display();
b.display(); // same output: both accounts show balance = 35.00 Euro
a.makeLodgement(100.0);
a.display(); // a now has a balance of 135.00 Euro
b.display(); // b still has balance of 35.00 Euro
}

The full source code is provided in defaultCopyConstructor.cpp. In this example, object b is created as a separate object in memory with identical state values to a. Changes to a do not affect b, and vice versa, since they are distinct objects.

Why Modify the Copy Constructor? While the default copy constructor works in many cases, sometimes it provides too little functionality. For example, suppose we want to create a new account object for the same owner, but with a fresh account number and a zero balance. In such cases, we can define our own copy constructor to specify custom copy behaviour.

class Account {
protected:
int accountNumber;
float balance;
string owner;
public:
Account(string owner, float aBalance, int anAccountNumber);
Account(float aBalance, int anAccountNumber);
Account(int anAccountNumber);
Account(const Account &sourceAccount); // custom copy constructor
...
};
Account::Account(const Account &sourceAccount) :
accountNumber(sourceAccount.accountNumber + 1), // new account number
balance(0.0f), // zero balance
owner(sourceAccount.owner) {} // copy owner name

The full source code is provided in modifiedCopyConstructor.cpp. Using the same main() function as before, the output is:

account number: 34234324 has balance: 35 Euro
This account is owned by: Derek Molloy
account number: 34234325 has balance: 0 Euro
This account is owned by: Derek Molloy
account number: 34234324 has balance: 135 Euro
This account is owned by: Derek Molloy
account number: 34234325 has balance: 0 Euro
This account is owned by: Derek Molloy

Here, the copy constructor copies the owner name but resets the balance and generates a new account number.

Key Points about Copy Constructors \

  • The default copy constructor performs a shallow, member-wise copy.
  • A user-defined copy constructor allows you to specify custom copy semantics.
  • Copy constructors are called when:
    • An object is initialised directly from another object (e.g. Account b(a);).
    • An object is passed by value to a function.
    • An object is returned by value from a function.
  • If your class manages resources such as dynamic memory or file handles, you should almost always implement a custom copy constructor (and a matching assignment operator and destructor) to ensure deep copies and avoid resource issues (this is known as the Rule of Three).
Concept Match

Match Copy and Resource Concepts

Move Semantics and the Rule of Five (C++11)

Section titled “Move Semantics and the Rule of Five (C++11)”

C++11 introduced move semantics — the ability to transfer ownership of resources from one object to another rather than copying them. This matters most for objects that manage heap-allocated memory, file handles, or network sockets, where copying is expensive and the source object is about to be discarded anyway.

Consider a class that owns a dynamically allocated buffer. Copying it requires allocating new memory and copying every element. Moving it simply transfers the pointer — the source is left empty.

Move Constructor

A move constructor takes an rvalue reference (T&&) and steals the source object’s resources:

class Buffer {
int* data;
size_t size;
public:
Buffer(size_t n) : size(n), data(new int[n]) {}
// Move constructor — steal the data, leave source safe to destruct
Buffer(Buffer&& source) noexcept
: data(source.data), size(source.size) {
source.data = nullptr;
source.size = 0;
}
~Buffer() { delete[] data; }
};

After the move, source.data is nullptr. The source is in a valid but empty state, and its destructor will safely do nothing.

Move Assignment Operator

Buffer& operator=(Buffer&& source) noexcept {
if (this != &source) {
delete[] data; // release the current resource first
data = source.data;
size = source.size;
source.data = nullptr;
source.size = 0;
}
return *this;
}

The Rule of Five

The Rule of Three extends to the Rule of Five in modern C++: if a class defines any one of these five special member functions, it should explicitly define or delete all five.

Special functionTypical trigger
DestructorObject goes out of scope or delete is called
Copy constructorObject b(a); or pass/return by value
Copy assignmentb = a;
Move constructorObject b(std::move(a)); or return from a function
Move assignmentb = std::move(a);

std::move(a) is a cast that produces an rvalue reference, signalling that a’s resources may be transferred rather than copied. After a move, a is in a valid but unspecified state and should not be used unless it is reassigned.

Code Cloze
C++

Defining a Move Constructor

Quiz
Select 0/1

What is the actual technical function of std::move when applied to an object in modern C++?

Quiz
Select 0/1

According to the Rule of Five introduced in C++11, which special member functions should typically be managed together?

The scope resolution operator in C++ (::) is used to explicitly specify the scope (namespace or class) of a variable or method.

Example: Accessing Global vs Local Variables

int x; // global variable
class A {
int x; // member variable
virtual void someMethod() {
x++; // increments A::x
::x++; // increments the global x
}
};

Here, x++ refers to the class member A::x, while ::x++ refers to the global variable x.

Example: Overloading and Hiding \n The scope resolution operator is also useful when a derived class hides methods from its base class (rather than overriding them).

class A {
public:
virtual int f(int);
};
class B : public A {
public:
virtual int f(char*); // hides A::f(int)
};
int main() {
B b;
b.f(2); // Error: B::f(char*) hides A::f(int)
b.A::f(2); // OK: explicitly call A::f(int)
b.f("test"); // OK: calls B::f(char*)
}

In this example:

  • B::f(char*) hides A::f(int) because the signatures differ.
  • Without scope resolution, b.f(2) fails since B::f(int) does not exist.
  • Using b.A::f(2) forces the compiler to call the base class version.
Concept Match

Match Scope and Keyword Concepts