Home | Projects | Notes > C++ Programming > Operator Overloading
C++ allows the programmer to overload most operators to work with user-defined classes. This is intended to make code more readable and writable by allowing the use of familiar operators in a familiar manner, but with our own classes and objects.
What is operator overloading?
Overloading the assignment operator (=)
Copy semantics
Move semantics
Overloading operators as member functions
Overloading operators as global functions
Overloading stream insertion (<<) and extraction operators (>>)
Using traditional operators such as +, =, *, etc. with user-defined types.
Allows user-defined types to behave and feel similar to built-in types
Can make code more readable and writable
Not done automatically (except for the assignment operator)
They must be explicitly defined.
The only operator that the compiler provides a default implementation for is the assignment operator (=). This is because the compiler must be able to assign one object to another. All the other operators that can be overloaded must be explicitly defined by the programmer.
Suppose we have a Number class that models any number.
Using functions:
1Number result = multiply(add(a, b), divide(c, d));Using member functions:
11Number result = (a.add(b)).multiply(c.divide(d));These statements are very unreadable and very complicated to write. Why can't we use the basic arithmetic symbols we have been using since elementary school now?
Using overloaded operators:
11Number result = (a + b) * (c / d);Now it looks and feels and behaves like the built-in C++ types.
Operator overloading is syntactic sugar. Behind the scenes, we're still calling functions.
The majority of C++'s operators can be overloaded.
The following operators CANNOT be overloaded:
:: (scope resolution operator)
:? (conditional operator)
.* (pointer to member operator)
. (dot operator)
sizeof
Remember, just because an operator can be overloaded doesn't mean you should. Don't overload it unless it makes sense and makes your code more usable, more readable and more writable.
We can make the operator mean anything we want. We want to make sure that when we do overload operators it makes sense and the users of the class know about it.
Precedence and Associativity CANNOT be changed.
"-arity" CANNOT be changed. (i.e., can't make the division operator "unary")
Can't overload operators for primitive type (e.g., int, double, etc.)
Can't create new operators
[], (), ->, and the assignment operator (=) MUST be declared as member functions.
Other operators can be declared as member functions or global functions.
Operator Overloading used on C++ Built-In Types:
int
31a = b + c2a < b3std::cout << adouble
31a = b + c2a < b3std::cout << along
31a = b + c2a < b3std::cout << aOperator Overloading used on User-Defined Types:
std::string
31s1 = s2 + s32s1 < s23std::cout << s1Player
31p1 < p22p1 == p23std::cout << p1Mystring
41s1 = s2 + s32s1 < s23s1 == s24std::cout << s1Mystring class declaration: (Incomplete)
See Complete Mystring Class section for the complete class implementation.
x123
4class Mystring5{6private:7 char *str; // pointer to a char[] that holds a C-style string8
9public:10 Mystring(); // default constructor11 Mystring(const char *s); // parameterized constructor12 Mystring(const Mystring &source); // copy constructor13 ~Mystring(); // destructor14 void display() const;15 int get_length() const; // getter16 const char* get_str() const; // getter17};18
19// MYSTRING_HMystring class implementation: (Incomplete)
See Complete Mystring Class section for the complete class implementation.
601// behind the scenes, Mystring class uses c-string library234
5// default constructor6Mystring::Mystring()7 :str(nullptr)8{9 str = new char[1];10 *str = '\0';11}12
13// parameterized constructor14Mystring::Mystring(const char *s)15 :str(nullptr)16{17 // if an empty string has been passed, create an empty string ""18 if (s == nullptr)19 {20 str = new char[1];21 *str = '\0';22 }23 else24 {25 str = new char[std::strlen(s) + 1];26 std::strcpy(str, s);27 }28}29
30// copy constructor31Mystring::Mystring(const Mystring &source)32 : str(nullptr)33{34 str = new char[std::strlen(source.str) + 1];35 std::strcpy(str, source.str);36}37
38// destructor39Mystring::~Mystring()40{41 delete[] str;42}43
44// display method45void Mystring::display() const 46{47 std::cout << str << ":" << get_length() << std::endl;48}49
50// length getter51int Mystring::get_length() const 52{ 53 return std::strlen(str);54}55
56// string getter57const char* Mystring::get_str() const58{59 return str;60}Test driver:
See Complete Mystring Class section for the complete class implementation.
15123
4int main(int argc, char *argv[])5{6 Mystring empty; // default constructor7 Mystring larry("Larry"); // overloaded constructor8 Mystring stooge(larry); // copy constructor9
10 empty.display();11 larry.display();12 stooge.display();13
14 return 0;15}31:02Larry:53Larry:5
When you don't provide a user-defined copy assignment operator, C++ will generate a default assignment operator used for assigning one object to another.
51Mystring s1("Frank");2Mystring s2 = s1; // NOT assignment because s2 hasn't been created yet!3 // same as Mystring s2(s1);4
5s2 = s1; // assignment since s2 has already been created and initializedDon't confuse "initialization" with "assignment". Initialization is done by constructors when we create new objects.
Default behavior is memberwise assignment (shallow copy).
If we have raw pointer data member, we must deep copy.
Copy assignment operator works with L-value references.
Overloading the copy assignment operator (deep copy):
11Type &Type::operator=(const Type &rhs);Example:
51Mystring& Mystring::operator=(const Mystring &rhs); // method name is 'operator='2
3s2 = s1; // we write this4s2.operator=(s1); // compiler will convert 's2 = s1' into this5 // 'operator=' method is calledReturning the reference is important because we don't want to make an extra copy of what we are returning and we want to allow chain assignments such as
s1 = s2 = s3.
The object on the left-hand side of an assignment statement is referred to by the this pointer. The object on the right-hand side is being passed into the method. The semantics is that the object on the LHS is going to be overwritten by the object on the RHS.
First, we need to deallocate anything it refers to on the heap. (If we don't do this, we'll orphan this memory and end up with a memory leak.)
Then we need to allocate the space in the LHS object for the RHS side object's data, and then we finally copy the data over to the left side from the right side.
Overloaded copy assignment operator for Mystring class:
131Mystring& Mystring::operator=(const Mystring &rhs)2{3 // check for self assignment4 if (this == &rhs)5 return *this; // if so, return current object6
7 delete[] str; // deallocate storage for 'this->str' since we are overwriting it8 // delete[] this->str; also works9 str = new char[std::strlen(rhs.str) + 1]; // allocate storage for the deep copy10 std::strcpy(str, rhs.str); // perform the copy11
12 return *this; // return the LHS object by reference to allow chain assignment13}Test driver:
See Complete Mystring Class secction for the complete class implementation.
13123
4int main(int argc, char *argv[])5{6 Mystring a("Hello"); // parameterized constructor7 Mystring b; // default constructor 8 b = a; // copy assignment operator9 // b.operator=(a)10 b = "This is a test"; // b.operator=("This is a test");11
12 return 0;13}In line
, the copy assignment operator will be called, a temporary object will be created and it will be assigned over. Once the assignment is done, the destructor will be called to destroy the temporary object.
You can choose to overload the move assignment operator.
C++ will use the copy assignment operator (copy assignment operator) by default, if necessary.
1Mystring s1; // empty string2s1 = Mystring("Frank"); // move assignment will be called since we are providing R-value reference 3 ----------------- -----------------4 temporary object is created "Frank"If we have raw pointer we should overload the move assignment operator for efficiency.
Move assignment operator works with R-value references. (Think temporary unnamed objects!)
Overloading the move assignment operator:
41Type& Type::operator=(Type &&rhs);2 --3 to tell the compiler that the right-side object is an R-value4 so the right-side value will be an R-value referenceRvalue reference
&&(C++11 and later)
Used in type declarations to create rvalue references.
Enables move semantics, which avoids unnecessary copies.
Note that the RHS object CANNOT be const since we'll be modifying that object when we move the data. (i.e., Nullifying the pointer)
Example:
31Mystring& Mystring::operator=(Mystring &&rhs);2s1 = Mystring("Joe"); // move operator= called3s1 = "Frank" // move operator= called111Mystring &Mystring::operator=(Mystring &&rhs)2{3 // check for self assignment4 if (this == &rhs) // self assignment5 return *this; // return current object6
7 delete[] str; // deallocate current storage8 str = rhs.str; // steal the pointer (NOT a deep copy)9 rhs.str = nullptr; // null out the rhs object (to prevent memory leak)10 return *this; // return current object11}Very similar to the copy assignment operator except that with move assignment operator we are NOT doing the deep copy. Instead, we're simply stealing the pointer and then nulling out the RHS pointer. Much more efficient (less overhead) than copy assignment.
Test driver:
See Complete Mystring Class secction for the complete class implementation.
123
4int main(int argc, char *argv[])5{6 Mystring a("Hello"); // overloaded (parameterized) constructor7 a = Mystring("Hola"); // overloaded constructor then move assignment8 // - a temporary unnamed object gets created with "Hola"9 // and destroyed after move assignment operation is done10 a = "Bonjour"; // overloaded constructor then move assignment11 // - a temporary unnamed object gets created with12 // "Bonjour" and destroyed after move assignment13 // operation is done14 return 0;15}If the move constructor, move assignment operator are not defined, this code will invoke copy constructor (or the copy assignment operator) instead.
C++ allows us to overload operators as
Member functions
Global non-member functions
Here, let's take a look at overloading operators as member functions.
Overloading unary operators as member functions (++, --, -, !):
Unary operators work on one operand.
31ReturnType Type::operatorOp();2 --3 operator goes here!In the case the we have to return a new object from the method, we'll return the new object by value.
Example:
91Number Number::operator-() const; 2Number Number::operator++(); // pre-increment3Number Number::operator++(int); // post-increment4bool Number::operator!() const;5
6Number n1(100);7Number n2 = -n1; // n1.operator-()8n2 = ++n1; // n1.operator++()9n2 = n1++; // n1.operator++(int)Notice that unary member functions have an empty parameter list. This is because the object we're working with is referred to by the
thispointer. Also, the keywordintis used in the parameters for the post-increment to differentiate it from the pre-increment.
Example:
81Mystring jack1{"JACK"};2Mystring jack2;3
4jack1.display(); // JACK5jack2 = -jack1; // jack1.operator-()6
7jack1.display(); // JACK8jack2.display(); // jackNote what the negation operator
-does when used with a string object.
Overloading binary operators as member functions (+, -, ==, !=, <, >, etc.):
31ReturnType Type::operatorOp(const Type &rhs);2 --3 operator goes here!Example:
91Number Number::operator+(const Number &rhs) const;2Number Number::operator-(const Number &rhs) const;3bool Number::operator==(const Number &rhs) const;4bool Number::operator<(const Number &rhs) const;5
6Number n1(100), n2(200);7Number n3 = n1 + n2; // n1.operator+(n2)8n3 = n1 - n2; // n1.operator-(n2)9if (n1 == n2) . . . // n1.operator==(n2)Now we have a single parameter in the method parameter list. Of course the this pointer points to the LHS operand.
Implementation of operator==:
91bool Mystring::operator==(const Mystring &rhs) const2{3 if (std::strcmp(str, rhs.str) == 0) // <cstring>4 return true;5 else6 return false;7}8
9// if (s1 == s2) // s1 and s2 are Mystring objectsImplementation of operator-: (make lowercase)
1Mystring Mystring::operator-() const2{3 char *buff = new char[std::strlen(str) + 1]; // Allocate memory space4 std::strcpy(buff, str); // Copy the string5 for (size_t i = 0; std::strlen(buff); i++) // Make each character lowercase6 buff[i] = std::tolower(buff[i]); // tolower() is in <ccpyte> header file7 Mystring temp(buff); // Create a Mystring obj using lowercase string as the initializer8 delete[] buff; // Delete the buffer created to prevent leak memory9 return temp; // Return the newly created Mystring object10}Implementation of operator+: (concatenation)
101Mystring Mystring::operator+(const Mystring &rhs) const2{3 size_t buff_size = std::strlen(str) + std::strlen(rhs.str) + 1;4 char *buff = new char[buff_size]; // Allocate large enough memory space for two strings5 std::strcpy(buff, str);6 std::strcat(buff, rhs.str);7 Mystring temp(buff);8 delete[] buff;9 return temp;10}101Mystring larry("Larry");2Mystring moe("Moe");3Mystring stooges(" is one of the three stooges");4
5Mystring result = larry + stooges; // larry.operator+(stooges);6result = moe + " is also a stooge"; // moe.operator+("is also a stooge");7
8result = "Moe" + stooges; // Illegal! "Meo".operator+(stooges) 9 // -----10 // not an obj of the class this fcn is called onNotice that we can also use c-style strings on the right-hand side. This is because we have a mystring constructor that can construct mystring objects from a c-style string.
The only limitation to overloading operators as member functions is that the object on the left-hand side must be an object of the class you are using.
These are no longer member functions, so we do not have this pointer referring to the object on the left-hand side.
Since we very often need access to private attributes in the objects, these non-member functions are often declared as friend functions of the class in many applications. (This isn't absolutely necessary since we can still use getter functions to access attribute values.)
When you have both the member and non-member versions of an overloaded operator at the same time, make sure that the cases in which they are applied are distinctive. (e.g., Applicable operand types are different, etc.) Otherwise, the compiler wouldn't know which one to use and would throw an error or warning upon encountering one.
Overloading unary operators as global functions(++, --, -, !):
31ReturnType operatorOp(Type &obj)2 --3 operator goes here!Example:
91Number operator-(const Number &obj);2Number operator++(Number &obj); // pre-increment3Number operator++(Number &obj, int); // post-increment4bool operator!(const Number &obj);5
6Number n1(100);7Number n2 = -n1; // operator-(n1)8n2 = ++n1; // operator++(n1)9n2 = n1++; // operator++(n1, int)In the case of the unary operators, a single object is in the parameter list. In the case of the binary operators, two objects are in the parameter list.
Implementation of operator - as a global function: (make lowercase)
It is assumed that this function has been declared as a friend to the Mystring class.
101Mystring operator-(const Mystring &obj)2{3 char *buff = new char[std::strlen(obj.str) + 1];4 std::strcpy(buff, obj.str);5 for (size_t i = 0; i < std::strlen(buff); i++)6 buff[i] = std::tolower(buff[i]);7 Mystring temp(buff);8 delete[] buff;9 return temp;10}You CANNOT have both the member and non-member versions of this overloaded operator at the same time. Or the compiler wouldn't know which one to use. You can only have one or the other.
Overloading binary operators as global functions (+, -, ==, !=, <, >, etc.):
31ReturnType operatorOp(const Type &lhs, const Type &rhs);2 --3 operator goes here!Example:
91Number operator+(const Number &lhs, const Number &rhs);2Number operator-(const Number &lhs, const Number &rhs);3bool operator==(const Number &lhs, const Number &rhs);4bool operator<(const Number &lhs, const Number &rhs);5
6Number n1(100), n2(200);7Number n3 = n1 + n2; // operator+(n1, n2)8n3 = n1 - n2: // operator-(n1, n2)9if (n1 == n2) ... // operator==(n1, n2)Implementation of operator==:
71bool operator==(const Mystring &lhs, const Mystring &rhs)2{3 if (std::strcmp(lhs.str, rhs.str) == 0)4 return true;5 else6 return false;7}If declared as a friend of
Mystring, it can access a private attributestr. Otherwise a getter function must be used.The code is almost the same as it was for the member version except that now it has an LHS object instead of the
thispointer.
Implementation of operator+ (concatenation):
181Mystring operator+(const Mystring &lhs, const Mystring &rhs)2{3 size_t buff_size = std::strlen(lhs.str) + std::strlen(rhs.str) + 1;4 char *buff = new char[buff_size];5 std::strcpy(buff, lhs.str);6 std::strcat(buff, rhs.str);7 Mystring temp(buff);8 delete[] buff;9 return temp;10}11Mystring larry("Larry");12Mystring moe("Moe");13Mystring stooges(" is one of the three stooges");14
15Mystring result = larry + stooges; // larry.operator+(stooges);16result = moe + " is also a stooge"; // moe.operator+("is also a stooge");17
18result = "Moe" + stooges; // OK with non-member functionsNotice that now the very last line of code is legal with the global (non-member) overloading function.
Test driver:
321234
5using namespace std;6
7int main(int argc, char *argv[])8{9 Mystring larry("Larry");10 larry.display(); // Larry11
12 larry = -larry;13 larry.display(); // larry14
15 cout << boolalpha << endl;16 Mystring moe("Moe");17 Mystring stooge = larry;18
19 cout << (larry == moe) << endl; // false20 cout << (larry == stooge) << endl; // true21
22 //Mystring stooges = larry + "Moe";23 Mystring stooges = "Larry" + moe; // Legal with non-member function24 stooges.display(); // LarryMoe25
26 Mystring two_stooges = moe + " " + Larry;27 two_stooges.display(); // Moe Larry28 Mystring three_stooges = moe + " " + larry + " " + "Curly";29 three_stooges.display(); // Moe larry Curly30
31 return 0;32}
<<, >>)This will allow us to insert/extract our Mystring objects to/from streams. (This makes our classes feel and behave more like a built-in C++ type.)
Doesn't make sense to implement as member functions.
Overloading operators as member functions requires left operand to be a user-defined class but this is not the way insertion/extraction operators are normally used.
21Mystring jack;2jack << cout; // Hun? Not consistent with how << is used with C++ built-in types21Mystring jack;2cout << jack; // This is the way << is used with C++ built-in typesMakes more sense to overload these operators as global functions!
Overloading stream insertion operator:
61std::stream& operator<<(std::ostream &os, const Mystring &obj)2{3 os << obj.str; // if friend function4 //os << obj.get_str(); // if not friend function, then must use the public getter function5 return os;6}Return a reference to the
ostream(i.e., output stream reference) so we can keep inserting. Don't returnostreamby value! (This will incur unnecessary copying of the stream.)
Overloading stream insertion operator:
1std::istream& operator>>(std::istream &is, Mystring &obj)2{3 char *buff = new char[1000];4 is >> buff;5 obj = Mystring(buff); // if copy or move assignment are predefined6 delete[] buff;7 return is;8}We want to modify the passed object so this function should NOT be
const.Update the object passed in.
Return a reference to the
istreamso we can do chain insert.Depending on the data we want to read, we can get the data from the input stream and either store it locally or store it directly in the object.
Test driver:
23123
4using namespace std;5
6int main(int argc, char *argv[])7{8 Mystring larry("Larry");9 Mystring moe("Moe");10 Mystring curly;11
12 cout << "Enter the third stooge's first name: ";13 cin >> curly;14
15 cout << "The three stooges are " << larry << " " << moe << " " << curly << endl;16
17 cout << "\nEnter the three stooges names separated by a space: ";18 cin >> larry >> moe >> curly;19
20 cout << "The three stooges are " << larry << " " << moe << " " << curly << endl;21
22 return 0;23}
Mystring Class==, -, + oerators are overloaded as both member and non-member functions. As this can cause some ambiguity issues, it would be safe to test (or comment out) one group at a time.
Mystring class declaration:
34123
4class Mystring5{6 friend bool operator==(const Mystring &lhs, const Mystring &rhs);7 friend Mystring operator-(const Mystring &obj);8 friend Mystring operator+(const Mystring &lhs, const Mystring &rhs);9 friend std::ostream& operator<<(std::ostream &os, const Mystring &rhs);10 friend std::istream& operator>>(std::istream &is, Mystring &rhs);11
12private:13 char *str; // pointer to a char[] that holds a C-style string14
15public:16 Mystring(); // default constructor17 Mystring(const char *s); // parameterized constructor18 Mystring(const Mystring &source); // copy constructor19 Mystring(Mystring &&source); // move constructor20 ~Mystring(); // destructor21
22 Mystring& operator=(const Mystring &rhs); // copy assignment operator overloading23 Mystring& operator=(Mystring &&rhs); // move assignment operator overloading24
25 Mystring operator-() const; // make lowercase26 Mystring operator+(const Mystring &rhs) const; // concatenate27 bool operator==(const Mystring &rhs) const; // equality operator28
29 void display() const;30 int get_length() const; // getter31 const char* get_str() const; // getter32};33
34// MYSTRING_HMystring class implementation:
1731// behind the scenes, Mystring class uses c-string library234
5// default constructor6Mystring::Mystring()7 :str(nullptr)8{9 str = new char[1];10 *str = '\0';11}12
13// parameterized constructor14Mystring::Mystring(const char *s)15 :str(nullptr)16{17 // if an empty string has been passed, create an empty string ""18 if (s == nullptr)19 {20 str = new char[1];21 *str = '\0';22 }23 else24 {25 str = new char[std::strlen(s) + 1];26 std::strcpy(str, s);27 }28}29
30// copy constructor31Mystring::Mystring(const Mystring &source)32 : str(nullptr)33{34 str = new char[std::strlen(source.str) + 1];35 std::strcpy(str, source.str);36}37
38// move constructor39Mystring::Mystring(Mystring &&source)40 :str(source.str) // stealing the pointer of the source object41{42 source.str = nullptr;43}44
45// destructor46Mystring::~Mystring()47{48 delete[] str;49}50
51// copy assignment operator52Mystring& Mystring::operator=(const Mystring &rhs)53{54 // check for self assignment55 if (this == &rhs)56 return *this; // if so, return current object57
58 delete[] str; // deallocate storage for 'this->str' since we are overwriting it59 // delete[] this->str; also works60 str = new char[std::strlen(rhs.str) + 1]; // allocate storage for the deep copy61 std::strcpy(str, rhs.str); // perform the copy62
63 return *this; // return the LHS object by reference to allow chain assignment64}65
66// move assignment operator67Mystring& Mystring::operator=(Mystring &&rhs)68{69 // check for self assignment70 if (this == &rhs) // self assignment71 return *this; // return current object72
73 delete[] str; // deallocate current storage74 str = rhs.str; // steal the pointer (NOT a deep copy)75 rhs.str = nullptr; // null out the rhs object (to prevent memory leak)76 return *this; // return current object77}78
79// equality80bool Mystring::operator==(const Mystring &rhs) const81{82 return (std::strcmp(str, rhs.str) == 0); 83}84
85// make lowercase86Mystring Mystring::operator-() const87{88 char *buff = new char[std::strlen(str) + 1];89 std::strcpy(buff, str);90 for (size_t i = 0; std::strlen(buff); i++)91 buff[i] = std::tolower(buff[i]);92 Mystring temp(buff);93 delete[] buff;94 return temp;95}96
97// concatenate98Mystring Mystring::operator+(const Mystring &rhs) const99{100 char *buff = new char[std::strlen(str) + std::strlen(rhs.str) + 1];101 std::strcpy(buff, str);102 std::strcat(buff, rhs.str);103 Mystring temp(buff);104 delete[] buff;105 return temp;106}107
108// display method109void Mystring::display() const110{111 std::cout << str << ":" << get_length() << std::endl;112}113
114// length getter115int Mystring::get_length() const116{117 return std::strlen(str);118}119
120// string getter121const char* Mystring::get_str() const122{123 return str;124}125
126// equality (global)127bool operator==(const Mystring &lhs, const Mystring &rhs)128{129 return (std::strcmp(lhs.str, rhs.str) == 0);130
131}132
133// make lowercase134Mystring operator-(const Mystring &obj)135{136 char *buff = new char[strlen(obj.str) + 1];137 std::strcpy(buff, obj.str);138 for (size_t i = 0; i < std::strlen(buff); i++)139 buff[i] = tolower(buff[i]);140 Mystring temp(buff);141 delete[] buff;142 return temp;143}144
145// concatenation146Mystring operator+(const Mystring &lhs, const Mystring &rhs)147{148 char *buff = new char[std::strlen(lhs.str) + std::strlen(rhs.str) + 1];149 std::strcpy(buff, lhs.str);150 std::strcat(buff, rhs.str);151 Mystring temp(buff);152 delete[] buff;153 return temp;154}155
156// stream insertion operator157std::ostream& operator<<(std::ostream &os, const Mystring &obj)158{159 os << obj.str; // if friend function160 //os << obj.get_str(); // if not friend function161 return os;162}163
164// stream extraction operator165std::istream& operator>>(std::istream &is, Mystring &obj)166{167 char *buff = new char[1000];168 is >> buff;169 obj = Mystring(buff); // since we have efficient move assignemt, it will be called170 // (steal the pointer)171 delete[] buff;172 return is;173}
Mitropoulos, F. (2022). Beginning C++ Programming - From Beginner to Beyond [Video file]. Retrieved from https://www.udemy.com/course/beginning-c-plus-plus-programming/