Home/Blog/Programming/C++ overloading: operators to overload as methods of a class
Home/Blog/Programming/C++ overloading: operators to overload as methods of a class

C++ overloading: operators to overload as methods of a class

16 min read
Jan 29, 2024
content
Introduction
Operators that can’t be nonmembers
Assignment operator
Subscript operator
Two ways to overload
Single overload
Noninteger index
Function call operator
Operators that are preferred as members
Wrapping up and next steps
Continue learning to code

Become a Software Engineer in Months, Not Years

From your first line of code, to your first day on the job — Educative has you covered. Join 2M+ developers learning in-demand programming skills.

Operator overloading is an interesting topic of object-oriented programming using C++. Operator overloading enables the objects of a class to be used in expressions. Some operators must be overloaded as non-static member functions of the class. This blog discusses these operators with coding examples.

Let’s dive right in!

We’ll cover:


Introduction#

Operators are overloaded as functions in C++. They can be overloaded in various ways: as member functions only, as nonmembers only, and as both members and nonmembers. Here, we discuss the operators that can be overloaded as non-static member functions only. The first operand or the caller of these operators is always an object of the class. Other aspects related to operator overloading are discussed in Operator Overloading in C++ and Various Ways of Overloading an Operator in C++.

The operators that can be overloaded in C++. The green subset must be overloaded as methods.
The operators that can be overloaded in C++. The green subset must be overloaded as methods.


Operators that can’t be nonmembers#

There are a few operators in C++ that must be overloaded as member functions of the class. The courses C++ Programming for Experienced Engineers and Data Structures Preliminaries (Refresher of Fundamentals in C++) discuss the topic of C++ operator overloading in greater detail. We’re discussing only the following operators here:

  • Assignment operator (=)

  • Subscript operator ([])

  • Function call operator (())

The caller object (the first operand) of all these operators is always an object of the class. Therefore, there’s no reason for their overloading from outside the class. We are using the example of an Array class that can store an array of integers. Initially, the Array class has the following members:

  • It has an int data member size to store the length of the array.

  • It has a pointer data member to store the int values.

  • It also has a constructor that can be invoked with one or two parameters. The first parameter specifies the size of the array to be allocated. The second parameter specifies the initial value to be assigned to all the elements of the array.

Note: The copy constructor for this class is not defined because it has no such usage here. Please refer to the course on Refresher of Fundamentals in C++ for further details.

  • It also has a destructor for proper deallocation of the memory allocated to its pointer data member.

  • It has a setAll() method to change the values of all the elements of the array to a single new value using a loop.

  • It also has a show() method to display the size and all the elements of the array using a loop.

Any other functionality will be added as the blog progresses. The next section discusses the implementation of the assignment operator.

C++ Programming for Experienced Engineers

Cover
C++ Programming for Experienced Engineers

If you’re looking to sharpen up your C++ programming skills, then you’re in the right place. You will start with learning about dynamic memory allocation, how to create classes, and constructors. In the latter half of the course, inheritance, functions, I/O, exception handling, and more. Throughout the course, you will study over 90 carefully designed examples aimed to enhance your understanding of the C++ language. By the time you finish this course, you will be able to use more advanced functionality in your C++ programs.

11hrs
Intermediate
12 Challenges
2 Quizzes


Assignment operator#

The assignment operator has two operands. The first operand is on the left-hand side of the operator, therefore termed the left-hand side (LHS). The second operand is on the right-hand side of the operator, therefore termed the right-hand side (RHS). The LHS is always a variable and is also termed LValue in this context. The type of LHS is always the class for which we want to overload the operator. The RHS can be a constant, variable, or expression. The type of RHS depends on the usage scenario that we want to permit to be assigned to the object.

The simplest (placeholder) overload of the assignment operator is given below to demonstrate the permissible overloading. We overload it for object-to-object assignment. It means that both the objects (LHS and RHS) are of the same type.

#include <iostream>
using namespace std;
class Array {
int size;
int * arr;
public:
~Array () {
delete [] arr;
}
Array (int sz, int val=0): size(1) {
if (sz > 1) size = sz;
arr = new int[size];
setAll(val);
}
void setAll(int v) {
for (int i=0; i<size; i++) {
arr[i] = v;
}
}
void show() const {
cout << size << " elements:";
for (int i=0; i<size; i++)
cout << " " << arr[i];
cout << endl;
}
void operator= (Array&) { } // does nothing
// static void operator= (Array&) { } // error
};
// void operator= (Array& , Array&) { } // error
int main() {
Array a(5, 1);
a.show();
Array b(8, 2);
b.show();
b = a; // Calling the overloaded assignment operator that does nothing for now
b.show();
return 0;
}

In the code above:

  • Lines 4–31: We define the Array class as having two data members and five methods.

    • Line 28: We provide an empty definition of the overloaded assignment operator just to emphasize its header.

    • Line 30: This is a commented line that indicates that the assignment operator can’t be overloaded as a static method. Uncommenting this line gives an error.

  • Line 33: This is also a commented line to indicate that the assignment operator can’t be defined outside the class (as a nonmember). Removing the // at the start causes an error in the program.

  • Lines 35–43: We define the main() function of the program to demonstrate the correct execution.

    • Line 36: We instantiate an object a to have a size 5 and the value 1 in all the elements of the array.

    • Line 37: We show the array values stored in a.

    • Line 38: We instantiate another object b to have a size 8 and the value 2 in all the elements of the array.

    • Line 39: We show the array values stored in b.

    • Line 40: We call the overloaded assignment operator, which has an empty body for the time being.

    • Line 41: We show the unchanged values of b because the overloaded assignment operator defined in the class does nothing.

If we don’t overload the assignment operator, then the default behavior provided by the language does the member-wise (or bit-wise) assignment of RHS onto LHS.

Commenting line 28 in the code above will cause an error due to the member-wise assignment provided by the default implementation of the assignment operator.

The following is the complete code to illustrate the overloaded assignment operator for the Array class. The overloaded operator performs the complete assignment and returns the LHS object with its updated values. Returning the object provides the facility of cascading or chaining, as in a = b = c;. For the successful execution of this expression, the result of b = c must return a value that can be stored in a. The cascading or chaining gets blocked if the operator has the return type void and returns nothing. Still, it can correctly perform the assignment.

#include <iostream>
using namespace std;
class Array {
int size;
int * arr;
public:
~Array () {
delete [] arr;
}
Array (int sz, int val=0): size(1) {
if (sz > 1) size = sz;
arr = new int[size];
setAll(val);
}
void setAll(int v) {
for (int i=0; i<size; i++) {
arr[i] = v;
}
}
void show() const {
cout << size << " elements:";
for (int i=0; i<size; i++)
cout << " " << arr[i];
cout << endl;
}
void resize(int sz) {
delete[] arr;
size = sz;
arr = new int[size];
}
const Array& operator= (const Array &rhs) {
if (size != rhs.size)
resize(rhs.size);
for (int i=0; i<size; i++) {
arr[i] = rhs.arr[i];
}
return *this;
}
};
int main() {
Array a(5, 1);
a.show();
Array b(8, 2);
b.show();
b = a; // Calling the overloaded assignment operator
b.show();
Array c(2, 0);
c.show();
Array d(5, 7);
d.show();
c = d = a; // Calling the operator twice: cascading or chaining
c.show();
d.show();
return 0;
}

Let’s start from the main() function in the above code:

  • Line 52: We invoke the overloaded assignment operator, where b is the calling object (LHS) and a is the parameter (RHS). The parameter is received as a const reference to the object for time and space efficiency.

  • Line 59: We demonstrate a cascaded call to the assignment operator. The assignment operator works from right to left. In the first step, d = a is called, then its result is sent as a parameter to the second call.

Now, let’s look at the working of the operator =() method:

  • Lines 38–39: First, we compare the size attribute of both objects (LHS and RHS). If the sizes are different, we call the resize() method to allocate the new memory to the calling object after deallocating its previously allocated one.

  • Line 41: Inside the loop, we copy each element of the RHS array to the LHS array elements.

  • Line 43: We return the updated value of the calling object through *this.

The following section discusses the peculiarities of implementing the subscript operator.

Data Structures Preliminaries (Refresher of Fundamentals in C++)

Cover
Data Structures Preliminaries (Refresher of Fundamentals in C++)

Dive into a comprehensive review of the fundamental elements of C++ through this course. Experience concise yet detailed explanations, coupled with hands-on exercises that delve into the core concepts of C++ fundamentals. Your journey begins with the exploration of fundamental programming concepts, such as the versatility of variables and their applications in strengthening control structures like conditions, loops, arrays and pointers. Next, you’ll cover structured programming, understanding its advantages and disadvantages, which seamlessly transitions you into object-oriented programming (OOP). Your focus will expand to encompass subjects like classes, object relationships and the implementation of operator overloading to equip user-defined data types. By the end of this course, you'll possess a solid grasp of the essential fundamentals, providing a robust foundation for success in subsequent courses, including the study of data structures and algorithms in C++.

30hrs
Beginner
151 Playgrounds
3 Quizzes


Subscript operator#

The following two subsections extend the Array class to illustrate the overloading of the subscript operator with a typical use case of an integer index. We throw an exception to indicate that the index value is out of range. The concluding part of this section illustrates the use of a noninteger index, like a map container, through overloading the subscript operator.


Two ways to overload#

There are two versions of a subscript operator: RValue and LValue. The following code demonstrates overloading the RValue usage for the Array class. In this scenario, we can use the subscript operator only on the RHS of the assignment.

#include <iostream>
#include <stdexcept> // to throw out_of_range exception
using namespace std;
class Array {
int size;
int * arr;
public:
~Array () {
delete [] arr;
}
Array (int sz, int val=0): size(1) {
if (sz > 1)
size = sz;
arr = new int[size];
setAll(val);
}
void setAll(int v) {
for (int i=0; i<size; i++) {
arr[i] = v;
}
}
void show() const {
cout << size << " elements:";
for (int i=0; i<size; i++)
cout << " " << arr[i];
cout << endl;
}
int operator[] (int index) const {
if (index < 0 || index >= size)
throw out_of_range("Index out of bounds");
return arr[index];
}
};
int main() {
Array a(5, 7);
a.show();
// Calls the overloaded RValue `Array [ int ]`
int val = a[3];
cout << "Value at index 3 is : " << val << endl;
// ERROR *** due to nonexistence of LValue overload for `Array [ int ]`
// a[2] = 20; // ERROR
return 0;
}

Let’s start from the main() function in the code above:

  • Line 42: We invoke the overloaded subscript operator, where a is the calling object and 3 is the parameter. The operator returns the value at index 3 of the array data member in object a.

  • Line 46: Uncommenting this line causes an error due to the absence of RValue overload of the subscript operator. This line is commented to enable the successful execution of the remaining code.

Now, let’s look at the working of the subscript operator (lines 30–34):

  • Line 30: The return type of the operator is int because we want to return the value of an element from the integer array attribute arr of the calling object a. The parameter int index gets the value 3. We declare this method const as it doesn’t change the calling object.

  • Line 31: We validate the value of the index. If it’s not within the boundaries, the next statement throws a predefined exception. C++ uses zero-based indexing, so the valid index values are from 0 to size - 1.

  • Line 32: We throw an out_of_range exception to indicate the issue. To use this predefined exception, we wrote #include <stdexcept> at the start of our program.

  • Line 33: We return the desired element of the array attribute. This statement executes when the index is within the limit as per the array size.

The following code illustrates the peculiarities of implementing the subscript operator for LValue usage. In this scenario, we can use the subscript operator on the LHS of an assignment statement.

We’re using a cout statement inside the overloaded operators to demonstrate their calling sequence for the reader. It’s not recommended for a normal overloading of these operators.

#include <iostream>
#include <stdexcept>
using namespace std;
class Array {
int size;
int * arr;
public:
~Array () {
delete [] arr;
}
Array (int sz, int val=0): size(1) {
if (sz > 1) size = sz;
arr = new int[size];
setAll(val);
}
void setAll(int v) {
for (int i=0; i<size; i++) {
arr[i] = v;
}
}
void show() const {
cout << size << " elements:";
for (int i=0; i<size; i++)
cout << " " << arr[i];
cout << endl;
}
// LValue implementation
int& operator[] (int index) {
cout << "LValue version " << endl; // The use of `cout` is not recommended here. This message is only to indicate its call for the reader.
if (index < 0 && index >= size)
throw out_of_range("Index out of bounds.");
return arr[index];
}
// RValue implementation
int operator[] (int index) const {
cout << "RValue version " << endl; // The use of `cout` is not recommended here. This message is only to indicate its call for the reader.
if (index < 0 && index >= size)
throw out_of_range("Index out of bounds.");
return arr[index];
}
};
int main() {
Array a(5, 7);
const Array c(5, 8);
// Displays the initial values of both objects
a.show();
c.show();
// Calls the overloaded RValue `Array [ int ]`
int val = c[3]; // the control goes to line 39
cout << "Now at line 57, the value at index 3 of c is : " << val << endl;
// Calls overloaded LValue `Array [ int ]`
a[3] = 9; // the control goes to line 31
cout << "Now at line 61, the value at index 3 of a is modified" << endl;
// ERROR *** due to the object being `const`
//c[2] = 9; // `const` object cannot be modified
// Displays the latest values of the object `a`
a.show();
return 0;
}

Let’s start from the main() function in the above code:

  • Line 56: We invoke the overloaded RValue subscript operator, where a constant object c is the caller and 3 is the parameter. The operator returns the value at index 3 of the array data member in the object c. We can directly use c[3] in the cout statement instead of storing it in a separate variable, val.

  • Line 60: We invoke the overloaded LValue subscript operator, where a is the calling object and 3 is the parameter. The operator returns the reference of the array element at index 3 in object a. The reference allows the RHS value to be stored in that element.

  • Line 64: Uncommenting this line causes an error because a const object can’t be modified. This line is commented to enable the successful execution of the remaining code.

Now, let’s look at both overloaded implementations of the subscript operator (lines 31–36 and lines 39–44):

  • Line 31: The return type of the operator is int& because we want to return the reference of an element from the integer array attribute arr of the calling object a. The parameter int index gets the value 3. We can’t declare this method const to permit a modification in the object value. The body of this method is exactly the same as discussed in the previous code.

  • Line 39: The return type of the operator is int because we want to return the value of an element from the integer array attribute arr of the const calling object c. The parameter int index gets the value 3. We must declare this method const to permit its call from a const object. The body of this method is exactly the same as demonstrated in the previous code.

Using a reference return type allows the change in the data member of an object. Therefore, such methods can’t be declared const.

The next subsection illustrates a single overload of the subscript operator that can work for LValue and RValue uses for non-constant objects. In this scenario, we can use it on both sides of an assignment statement.

Returning a reference to the private data of an object is a breach of encapsulation.


Single overload#

Yes, we can carry two faces under one hood. The LValue implementation given above can suffice both calls for non-constant objects. The following code illustrates the use of a single implementation for both uses (LValue and RValue).

#include <iostream>
#include <stdexcept>
using namespace std;
class Array {
int size;
int * arr;
public:
~Array () {
delete [] arr;
}
Array (int sz, int val=0): size(1) {
if (sz > 1) size = sz;
arr = new int[size];
setAll(val);
}
void setAll(int v) {
for (int i=0; i<size; i++) {
arr[i] = v;
}
}
void show() const {
cout << size << " elements:";
for (int i=0; i<size; i++)
cout << " " << arr[i];
cout << endl;
}
int& operator[] (int index) {
if (index < 0 && index >= size)
throw out_of_range("Index out of bounds.");
return arr[index];
}
};
int main() {
Array a(5, 7);
a.show();
// Calls overloaded `Time [ string ]`
cout << "Value at index 3 is : " << a[3] << endl;
// Calls overloaded `Time [ string ]`
a[2] = 20;
// Display the updated value of the object.
a.show();
return 0;
}

In the code above:

Lines 30–34: This is the single overloaded operator that’s called for both uses.

Line 42: This is the RValue use of the subscript operator.

Line 45: This is the LValue use of the subscript operator.

Line 48: This is what happens at the end: we show the updated value of the object.


Noninteger index#

Since we use an integer index for an array in C++, most of the time, it’s easy to perceive the usefulness of an integer parameter for the subscript operator. However, a noninteger parameter is also used with the subscript operator as an index.

The subscript operator is a binary operator. It must be defined for two operands only (neither more nor less). However, the type of the second operand can vary.

We use the example of the Time class for illustration. This class has the following members:

  • Three private data members, h, m, and s, to store the values of hours, minutes, and seconds, respectively.

  • Three private setter methods, setH(), setM(), and setS(), to modify the values of hours, minutes, and seconds, respectively, within the permissible value ranges.

  • A public constructor that receives three parameters to initialize the data members. It invokes the setter methods (for reusability and maintenance convenience).

  • A public method, show(), to display the values of data members separated by :. The fancy details of the display are ignored for simplicity and focus.


From this point onward, we’ll keep using and extending this minimal implementation of the Time class.

#include <iostream>
#include <stdexcept>
using namespace std;
class Time {
int h, m, s;
void setH(const int val) {h = val % 24;}
void setM(const int val) {m = val % 60;}
void setS(const int val) {s = val % 60;}
public:
Time (int hh, int mm, int ss): h(0), m(0), s(0) {
setH(hh);
setM(mm);
setS(ss);
}
void show() const { cout << h << ":" << m << ":" << s << " ";}
// Overloaded subscript operator with noninteger parameter
int& operator[] (const string &index) {
if (index != "hour" && index != "minute" && index != "second")
throw out_of_range("Invalid index.");
if (index == "hour") return h;
else if (index == "minute") return m;
return s;
}
};
int main() {
Time noon(12, 0, 0);
noon.show();
// Calls overloaded `Time [ string ]`
cout << "\nHour: " << noon["hour"] << endl;
// Calls overloaded `Time [ string ]`
noon["second"] = 30;
noon.show();
return 0;
}

In the code above:

  • Lines 6–29: We define a Time class as described before the code. Additionally, it defines the subscript operator (lines 21–28).

    • Line 21: This receives a string parameter to mention the specific attribute that we want to access for read/write. It accepts only "hour", "minute", or "second" as an argument. It throws an exception if it receives any other string.
  • Lines 31–43: We define the main() function that creates an object noon of type Time.

    • Line 36: We call the subscript operator to read the value of "hour" from noon.

    • Line 39: We call the subscript operator to write the value of "second" to noon.

Zero to Hero in C++

Cover
Become a C++ Programmer

C++ is a robust and flexible language commonly used for games, desktop, and embedded applications development. This Skill Path is perfect for beginners eager to learn C++ and embark on a programming journey. You’ll explore C++ fundamentals, starting with basic syntax and functionality to create programs, and then dive into more complex concepts like dynamic memory allocation in C++. The latter half focuses on C++ programming with a detailed overview of object-oriented programming (OOP), which includes classes in C++, data hiding in C++, encapsulation in C++, abstraction in C++, inheritance in C++, and polymorphism in C++. Hands-on practice with algorithms and data structures will empower you to write real-world programs confidently, paving your way as a C++ developer.

38hrs
Beginner
32 Challenges
46 Quizzes


Function call operator#

There’s another bracket operator (()) in C++. It’s termed the function call operator. It’s much more versatile. It allows for any type and any number of parameters. The following code demonstrates its five variations:

  • No parameter: It works as the show() method demonstrated above.

  • One parameter: It works with one parameter for two distinct functionalities.

    • An int parameter: It’s a variant of show() that displays the values in 12-hour format.

    • A string parameter: It works as the subscript operator demonstrated above.

  • Two parameters, string and int: It works as a setter method using an integer value for the string—"hour", "minute", or "second".

  • Three parameters: It receives three int parameters and updates all three data members with them.

The function call operator (()) has no restriction on parameters. It can be overloaded for any number of parameters of any type.

The following code demonstrates the implementation and usage of the abovementioned five variations of the function call operator.

#include <iostream>
#include <stdexcept>
using namespace std;
class Time {
int h, m, s;
void setH(const int val) {h = val % 24;}
void setM(const int val) {m = val % 60;}
void setS(const int val) {s = val % 60;}
public:
Time (const int hh, const int mm, const int ss)
: h(0), m(0), s(0) {
setH(hh);
setM(mm);
setS(ss);
}
void operator()() const {
cout << h << ":" << m << ":" << s << " ";
}
void operator()(const int) const {
if (h == 0)
cout << 12 << ":" << m << ":" << s << " AM" << endl;
else if (h >= 12)
cout << (h-12) << ":" << m << ":" << s << " PM" << endl;
else
cout << h << ":" << m << ":" << s << " AM" << endl;
}
int operator() (const string &index) {
if (index != "hour" && index != "minute" && index != "second")
throw out_of_range("Index should be: \"hour\", \"minute\", or \"second\".");
if (index == "hour") return h;
else if (index == "minute") return m;
return s;
}
void operator() (const string &index, const int value) {
if (index != "hour" && index != "minute" && index != "second")
throw out_of_range("Invalid index; it should be: \"hour\", \"minute\", or \"second\".");
if (index == "hour") setH(value);
else if (index == "minute") setM(value);
else if (index == "second") setS(value);
}
void operator() (const int vh, const int vm, const int vs) {
setH(vh);
setM(vm);
setS(vs);
}
};
int main() {
Time noon(12, 0, 0);
// Calls overloaded function call operator with no parameters
noon();
// Calls overloaded function call operator with a string parameter
int hour = noon("hour");
cout << "\nHour: " << hour << endl;
// Calls overloaded Time ( string , int )
noon("second", 30);
noon("hour", 17);
// Calls overloaded function call operator with an int parameter
noon(1);
// Calls overloaded function call operator with three int parameters
noon(10,20,30);
// Calls overloaded function call operator with no parameters
noon();
return 0;
}

In the code above:

  • Lines 19–21: We define the function call operator with no parameters. It’s just a renamed version of the show() method demonstrated above.

  • Lines 23–30: We define the function call operator with an int parameter. We don’t use the parameter inside the function body, so we don’t use a variable name for it. We simply use the presence of the argument to display the time using the AM/PM format.

  • Lines 32–39: We define the function call operator with a string parameter. We use it as the RValue version of the subscript operator. It returns the value of a specific data member based on the received string: "hour", "minute", or "second". In the case of any other string, it throws an out_of_range exception. If we set the return type of this operator to int&, it can also behave as an LValue implementation of the subscript operator.

  • Lines 41–48: We define the function call operator with two parameters. We use it as the setter method for each data member. It modifies the value of a specific data member with the received int parameter based on the received string: "hour", "minute", or "second". In the case of any other string, it also throws an out_of_range exception.

  • Lines 50–54: We define the function call operator with three parameters for the corresponding data members. We use it as the setter method for all data members in a single call.

In main():

  • Line 58: We create a noon object of the Time type.

  • Line 61: We call the function call operator with no parameters for noon to display the current value of the data members.

  • Line 64: We call the function call operator with a string argument "hour" to obtain the hour’s value of noon.

  • Lines 67–68: We call the function call operator with two arguments to set the values of seconds and minutes in the noon object to 30 and 17, respectively.

  • Line 71: We call the function call operator with no parameters for noon to display the current value of data members in the AM/PM format.

  • Line 74: We call the function call operator with three arguments to set the values of hours, minutes, and seconds in the noon object to 10, 20, and 30, respectively.

  • Line 77: We call the function call operator with no parameters for the noon object again to display the current value of the data members.

Due to the versatility of the function call operator, its usage makes programs difficult to understand without a standard reading.

Become a C++ Programmer

Cover
Become a C++ Programmer

C++ is a popular language used to develop browsers, games, and operating systems. It's also an essential tool in the development of modern technology like IoT and self-driving cars. This Path will expand your knowledge of C++ with lessons built for professional developers. This Path will take you from basic to advanced concepts with hands-on practice. By the end, you'll have enough C++ experience to confidently solve real-world problems.

119hrs
Beginner
82 Challenges
152 Quizzes


Operators that are preferred as members#

The compound assignment (+=, %=, and &=, etc.) and unary (-, ~, and !, etc.) operators always have an object as their first operand. The language doesn’t restrict them from being overloaded as nonmember functions. However, it’s better to keep them inside the class in support of encapsulation.


Wrapping up and next steps#

Some operators can’t be overloaded as nonmembers, e.g., assignment, subscript, and function call operators. The caller (the first operand) or these operators is always an object of the class. Therefore, there’s no point in overloading them as nonmembers.

The function call operator has no restriction on the number and the types of its parameters. The Standard Template Library (STL) uses this powerful feature of the language. Such objects that have overloaded function call operators are called functor objects.

Compound assignment and unary operators can be overloaded as nonmembers and members of the class despite the fact that their caller (the first operand) is also an object of the class. It’s a good programming practice to overload them as members.

Keep exploring, keep overloading, and happy learning!


To start learning these concepts and more, check out Educative’s Data Structures Preliminaries (Refresher of Fundamentals in C++) course.

Continue learning to code#


Written By:
Aasim Ali

Free Resources