Week 8
Operator overloading
Reference: TICPPv1, Chapter 12
Here is a very simple code that shows the essence of operator overloading: '+', '++ (post)', and '++ (pre)'.
- class A
- {
- public:
- A(){}
- explicit A(int n):data(n) {}
- int data;
- A operator+(A&);
- A operator++(int);
- A& operator++();
- };
- // + overloading
- A A::operator+(A& obj)
- {
- A tmp = *this;
- tmp.data = this->data + obj.data;
- return tmp;
- }
- // post increment (x++) overloading
- // returns original value, and then increment the value
- // copy needed
- // return a locally created object.
- // Note that it's not returning a reference since it's a temporary obj.
- A A::operator++(int)
- {
- A& tmp = *this;
- this->data = this->data + 1;
- return tmp;
- }
- // pre increment (++x) overloading
- // returns incremented tvalue
- // no copy necessary
- A& A::operator++()
- {
- this->data = ++(this->data);
- return *this;
- }
- int main()
- {
- A obj1(10);
- A obj2(20);
- A obj3 = obj1 + obj2; // obj3.data = 10 + 20 = 30
- A obj4 = obj1++; // obj4.data = 10, obj1.data = 11
- A obj5 = ++obj2; // obj5.data = 21, obj2.data = 21
- return 0;
- }
Operator overloading extends the function overloading concept to operators so that we can assign new meanings to C++ operators. It lets us extend operator overloading to user-defined types. That is by allowing us to use the "+" to add two objects. The compiler determines which definition of addition to use depending on the number and type of operands. Overloaded operators can often make code look more natural. In other words, operator overloading can be very useful to make our class look and behave more like built-in types.
To overload an operator, we use a special function, operator function. For example, when we overload "+":
operator+(argument_list)
Suppose, for example, that we have a MyComplex class for which we define an operator+() member function to overload the + operator so that it adds one complex number to another complex number. Then, if c1, c2, c3 are all objects of MyComplex class, we can write this:
c3 = c1 + c2;
The compiler, recognizing the operands as belonging to the MyComplex class, replaces the operator with the corresponding operator function:
c3 = c1.operator+(c2);
The function then use the c1 object which invokes the method, and the c2 object is passed as an argument to calculate the sum, and returns it. Note that we use assignment operator = which is also need to be overload.
Operators that cannot be overloaded
The following operators cannot be overloaded:
- . member selection
- .* member selection with pointer-to-member
- ?: conditional
- :: scope resolution
- # stringizing operator
- ## merging operator
- sizeof object size information
- typeid object type information
Overloading '='
In this section we'll learn how to overload the assignment (=) operator between two objects of the MyComplex class.
Let's look at the following code:
- class MyComplex
- {
- private:
- double real, imag;
- public:
- MyComplex(){
- real = 0;
- imag = 0;
- }
- MyComplex(double r, double i) {
- real = r;
- imag = i;
- }
- double getReal() const {
- return real;
- }
- double getImag() const {
- return imag;
- }
- MyComplex & operator=(const MyComplex &);
- };
- MyComplex & MyComplex::operator=(const MyComplex& c) {
- real = c.real;
- imag = c.imag;
- return *this;
- }
- #include <iostream>
- int main()
- {
- using namespace std;
- MyComplex c1(5,10);
- MyComplex c2(50,100);
- cout << "c1= " << c1.getReal() << "+" << c1.getImag() << "i" << endl;
- cout << "c2= " << c2.getReal() << "+" << c2.getImag() << "i" << endl;
- c2 = c1;
- cout << "assign c1 to c2:" << endl;
- cout << "c2= " << c2.getReal() << "+" << c2.getImag() << "i" << endl;
- }
We get the output as expected:
c1= 5+10i c2= 50+100i assign c1 to c2: c2= 5+10i
Overloading '+'
In this section we'll learn how to overload the addition (+) operator between two objects of the MyComplex class.
Let's look at the following code which has '+' additional overloading function:
- class MyComplex
- {
- private:
- double real, imag;
- public:
- MyComplex(){
- real = 0;
- imag = 0;
- }
- MyComplex(double r, double i) {
- real = r;
- imag = i;
- }
- double getReal() const {
- return real;
- }
- double getImag() const {
- return imag;
- }
- MyComplex & operator=(const MyComplex &);
- MyComplex & operator+(const MyComplex& );
- };
- MyComplex & MyComplex::operator=(const MyComplex& c) {
- real = c.real;
- imag = c.imag;
- return *this;
- }
- MyComplex & MyComplex::operator+(const MyComplex& c) {
- real += c.real;
- imag += c.imag;
- return *this;
- }
- #include <iostream>
- int main()
- {
- using namespace std;
- MyComplex c1(5,10);
- MyComplex c2(50,100);
- cout << "c1= " << c1.getReal() << "+" << c1.getImag() << "i" << endl;
- cout << "c2= " << c2.getReal() << "+" << c2.getImag() << "i" << endl;
- c2 = c1;
- cout << "assign c1 to c2:" << endl;
- cout << "c2= " << c2.getReal() << "+" << c2.getImag() << "i" << endl;
- cout << endl;
- MyComplex c3(10,100);
- MyComplex c4(20,200);
- cout << "c3= " << c3.getReal() << "+" << c3.getImag() << "i" << endl;
- cout << "c4= " << c4.getReal() << "+" << c4.getImag() << "i" << endl;
- MyComplex c5 = c3 + c4;
- cout << "adding c3 and c4" << endl;
- cout << "c3= " << c3.getReal() << "+" << c3.getImag() << "i" << endl;
- cout << "c4= " << c4.getReal() << "+" << c4.getImag() << "i" << endl;
- cout << "c5= " << c5.getReal() << "+" << c5.getImag() << "i" << endl;
- }
Note that when we're using '+' for the object of MyComplex type,
c5 = c3 + c4;
actually, we are calling a function like this:
c5 = c3.operator+(c4)
The output of the code above is:
c1= 5+10i c2= 50+100i assign c1 to c2: c2= 5+10i c3= 10+100i c4= 20+200i adding c3 and c4 c3= 30+300i c4= 20+200i c5= 30+300i
We got the right result at least for c5. But the value of c3 has been changed.
What happened?
Let look at the code overloading '+'.
- MyComplex & MyComplex::operator+(const MyComplex& c) {
- real += c.real;
- imag += c.imag;
- return *this;
- }
As it turned out, the operation inside the function returning the reference to c3 object which has been changed.
So, let's rewrite the overloading function.
- const MyComplex operator+(const MyComplex & );
- const MyComplex MyComplex::operator+(const MyComplex& c) {
- MyComplex temp;
- temp.real = this->real + c.real;
- temp.imag = this->imag + c.imag;
- return temp;
- }
Note that this doesn't return MyComplex &, but instead returns a const MyComplex class variable. As you can see, the implementation is a little bit different from the previous example. Here, we're not returning *this
. Instead, we're creating a temporary variable and assigning the results of the addition to temp. This explains why the function returns const MyComplex and not MyComplex &. In other words, the function creates a new MyComplex object temp
that represents the sum of the other two MyComplex objects. Returning the object creates a copy of the object that the calling function can use. If the return type were MyComplex &, however, the reference would be the temp object. But the temp object is a local variable and is destroyed when the function returns, so the reference would be a reference to a nonexisting object. Using a MyComplex return type, however, means the program constructs a copy of MyComplex object before destroying it, and the calling function gets the copy.
Then, we'll get the right answer.
c1= 5+10i c2= 50+100i assign c1 to c2: c2= 5+10i c3= 10+100i c4= 20+200i adding c3 and c4 c3= 10+100i c4= 20+200i c5= 30+300i
Overloading the ostream (<<) operator and the istream (>>) operator
Output streams use the << operator for standard types. We can also overload the << operator for our own classes.
Actually, the << is left shift bit manipulation operator. But the ostream class overloads the operator, converting it into an output tool. The cout is an ostream object and that it is smart enough to recognize all the basic C++ types. That's because the ostream class declaration includes an overloaded operator<<()
definition for each of the basic types.
The istream operator can be overloaded almost the same way except the 2nd parameter is not const.
- #include <iostream>
- using namespace std;
- class MyComplex
- {
- private:
- double real, imag;
- public:
- MyComplex(){
- real = 0;
- imag = 0;
- }
- MyComplex(double r, double i) {
- real = r;
- imag = i;
- }
- double getReal() const {
- return real;
- }
- double getImag() const {
- return imag;
- }
- MyComplex & operator=(const MyComplex &);
- const MyComplex operator+(const MyComplex & );
- MyComplex & operator++(void);
- MyComplex operator++(int);
- /*friend const
- MyComplex operator+(const MyComplex&, const MyComplex&); */
- friend ostream& operator<<(ostream& os, const MyComplex& c);
- // note: no const for the second parameter
- friend istream& operator>>(istream& is, MyComplex& c);
- };
- MyComplex & MyComplex::operator=(const MyComplex& c) {
- real = c.real;
- imag = c.imag;
- return *this;
- }
- const MyComplex MyComplex::operator+(const MyComplex& c) {
- MyComplex temp;
- temp.real = this->real + c.real;
- temp.imag = this->imag + c.imag;
- return temp;
- }
- //pre-increment
- MyComplex & MyComplex::operator++() {
- real++;
- imag++;
- return *this;
- }
- //post-increment
- MyComplex MyComplex::operator++(int) {
- MyComplex temp = *this;
- real++;
- imag++;
- return temp;
- }
- /* This is not a member function of MyComplex class */
- /*
- const MyComplex operator+(const MyComplex& c1, const MyComplex& c2) {
- MyComplex temp;
- temp.real = c1.real + c2.real;
- temp.imag = c1.imag + c2.imag;
- return temp;
- }*/
- ostream& operator<<(ostream &os, const MyComplex& c) {
- os << c.real << '+' << c.imag << 'i' << endl;
- return os;
- }
- istream& operator>>(istream &is, MyComplex& c) {
- is >> c.real >> c.imag;
- return is;
- }
- int main()
- {
- MyComplex c1(5,10);
- cout << "c1 = " << c1.getReal() << "+" << c1.getImag() << "i" << endl;
- cout << "Using overloaded ostream(<<) " << endl;
- cout << "c1 = " << c1 << endl;
- MyComplex c2;
- cout << "Enter two numbers: " << endl;
- cin >> c2;
- cout << "Using overloaded istream(>>) " << endl;
- cout << "Input complex is = " << c2;
- return 0;
- }
The output is:
c1 = 5+10i Using overloaded ostream(<<) c1 = 5+10i Enter two numbers: 111 222 Using overloaded istream(>>) Input complex is = 111+222i Note that we just used: cout << "c1 = " << c1 << endl; Note that when we do cout << c1; it becomes the following function call: operator<<(cout, c1);
Return types for overloaded operators
Returning a const object
The MyComplex::operator+() in the example has a strange property. The intended use is this:
MyComplex c6 = c1 + c2; // #1
But the definition also allows us to use the following:
MyComplex c7; c1 + c2 = c7; // #2
This code is possible because the copy constructor constructs a temporary object to represent the return value. So, in the code, the expression c1 + c2
stands for that temporary object. In statement #1, the temporary object is assigned to c6. In statement #2, c7 is assigned to the temporary object.
The temporary object is used and then discarded. For instance, in statement #2, the program computes the sum of c1 and c2, copies the answer into the temporary return object, overwrites the contents with the contents of c7, and then discards the temporary object. The original complex numbers are all left unchanged.
If we declare the return type as a const object, we can avoid this problem and disallow the #2 case.
const MyComplex operator+(const MyComplex & c) const { return MyComplex(real + c.real, imag + c.imag); }
Why const with a reference return?
Let's look at the following example:
- #include <iostream>
- struct node
- {
- int data;
- };
- const node & makeNode(node &cref);
- int main()
- {
- node nodeA = {0};
- std::cout << "1: nodeA.data = " << nodeA.data << std::endl;
- makeNode(nodeA);
- std::cout << "2: nodeA.data = " << nodeA.data << std::endl;
- node nodeB;
- nodeB = makeNode(nodeA);
- std::cout << "3: nodeA.data = " << nodeA.data << std::endl;
- std::cout << "1: nodeB.data = " << nodeB.data << std::endl;
- node nodeC;
- // makeNode(nodeB).data = 99;
- return 0;
- }
- const node & makeNode(const node &ref)
- {
- std::cout << "call makeNode()\n";
- ref.data++;
- const return ref;
- }
The makeNode()
function return type is const node &
. What's the purpose of const? It does not mean that the node structure itself is const; it just means that we can't use the return value directly to modify the structure.
If we omitted const, we could use the code below:
makeNode(nodeB).data = 99;
Because makeNode
returns a reference to nodeB
, this code is the same as this:
makeNode(NodeB); nodeB.data = 99;
But because of the const, we can't use the return value directly to modify the structure, and with the const, the code:
makeNode(nodeB).data = 99;
won't compile.
Summary
- If a method or function returns a local object, it should return an object, not a reference.
- If a method or function returns an object of a class for which there is no public copy constructor, such as
ostream
class, it must return a reference to an object. - Some methods and functions, such as the overloaded assignment operator, can return either an object or a reference to an object. The reference is preferred for reasons of efficiency.
- It is an error to return a pointer to a local object. Once the function completes, the local objects are freed. The pointer would be a dangling pointer that refers to a nonexistent object.