C++ Tutorial
Operator Overloading i - 2020
A default argument is a value that will be used automatically if we omit the corresponding actual argument when we call a function.
How do we set a default value? We must use the function prototype because the compiler looks at the prototype to check how many arguments a function uses.
For example:
int f(const char *s, int n = 2013)We want the function to return a new int, so its type is int. We want n to have a default value of 2013, so we assign that value to n. If we leave n alone, it has the value 2013, but if we pass an argument, the new value overwrites the 2013
When we use a function with an argument list, we must add defaults from right to left. In other words, we can't provide a default value for a particular argument unless we also provide defaults for all the arguments to its right:
float f1(int l, int m = 2, int n = 3); // OK float f2(int l, int m = 2, int n); // Not OK
Function overloading lets us use multiple functions sharing the same name. We usually utilize the function overloading to design a family of functions that do the same thing while using different argument lists.
The key to function overloading is a function's argument list (function signature). If two functions use the same number and types of arguments in the same order, they have the same signature. C++ allows us to define two multiple functions by the same name, provided that the functions have different signatures. The signature can differ:
- in the number of arguments
- or in the type of arguments
- or both
For example, we defines several versions of f() with the following prototypes:
void f(const char *s, int n); // (a) void f(double d, int n); // (b) void f(long l, int n); // (c) void f(int m, int n); // (d) void f(const char *s); // (e)
Some signatures which appears to be different but actually both have the same signature:
double square(double d); double square(double &d;);
But we need to look with compiler's perspective. To call them, we use:
square(z);
and the z argument matches both double d and the double &d;, thus the compiler has no way of knowing which function to use. So, to avoid such ambiguity, when it checks function signatures, the compiler considers a reference to a type and the type itself to be the same signature.
Note that in the function-matching process, the compiler discriminate between const and non-const variables:
int f(char *s); // overloaded int f(const char *s); // overloaded
Also note that the signature, not the return type, enables function overloading. For instance, the following two have the same signature, and can't not be overloaded.
float f(int m, int *n) // Not overloaded double f(int m, int *n) // Not overloaded
Here is a very simple code that shows the essence of operator overloadings: '+', '++ (post)', and '++ (pre)'. If you do not understand what's going on in the code, please do not worry, at the end of this chapter, you will know how it works.
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)++; 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 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.
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
In this section we'll learn how to overload the assignment (=) operator between two objects of the Complex 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
In this section we'll learn how to overload the addition (+) operator between two objects of the Complex 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 something like this.
c5 = c3.operator+(c4)
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 Complex &, but instead returns a const Complex 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 Complex and not Complex &. 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 terminates, 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.
Why we're returning const? Look Object Returning.
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
We get the same result if we use a friend function which is global and not a member of our MyComplex class.
Since this function will need to access the private members of MyComplex, we'll need to declare it as a friend function.
friend const MyComplex operator+(const MyComplex&, const MyComplex&);
The prototype has two implications:
- Although the operator+() function is not a member function, it has the same access rights as a member function.
- Although the operator+() function is declared in the class declaration, it is not a member function. So it isn't invoked by using the membership operator.
Our revised code is:
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 &); friend const MyComplex operator+(const MyComplex&, const MyComplex&); }; MyComplex & MyComplex::operator=(const MyComplex& c) { real = c.real; imag = c.imag; return *this; } /* 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; } #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, in the above example, we used one of the two forms for overloading operator+().
What we used was nonmember version:
MyComplex operator+(const MyComplex& c1, const MyComplex& c2);
But there is another form which is member version:
MyComplex operator+(const MyComplex& c1);
For the member function version, one is passed implicitly via the this pointer and the second is passed explicitly as a function argument. For the friend version, which is nonmember version, both are passed as argument.
Either of these two prototypes matches the express c1+c2, where c1 and c2 are type MyComplex objects. That is, the compiler can convert the statement
c3 = c1 + c2;
to either of the following:
c3 = opetator+(c1,c2); // nonmember function c3 = c1.operator+(c2); // member function
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 does not have 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; }
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);
When we want to serialize (save() and then load()) a class, we need to overload two operators << and >> because the operators do know what to do with a class. Here is an example using Qt5:
#include <QCoreApplication> #include <QFile> #include <QString> #include <QDataStream> #include <QDebug> class Student { public: int ID; QString Name; // ostream, << overloading friend QDataStream &Student;::operator<<(QDataStream &out;, const Student &s;) { out << s.ID << s.Name; return out; } // istream, >> overloading friend QDataStream &Student;::operator>>(QDataStream ∈, Student &s;) { s = Student(); in >> s.ID >> s.Name; return in; } }; void Save() { Student s1; s1.ID = 1; s1.Name = "Ravel"; Student s2; s2.ID = 2; s2.Name = "Schonberg"; QString filename = "C:/Qt/Test/st.txt"; QFile file(filename); if(!file.open(QIODevice::WriteOnly)) { qDebug() << "Could not open " << filename; return; } QDataStream out(&file;); out.setVersion(QDataStream::Qt_5_1); out << s1 << s2; file.flush(); file.close(); } void Load() { Student s1; Student s2; s2.ID; s2.Name; QString filename = "C:/Qt/Test/st.txt"; QFile file(filename); if(!file.open(QIODevice::ReadOnly)) { qDebug() << "Could not open " << filename; return; } QDataStream in(&file;); in.setVersion(QDataStream::Qt_5_1); in >> s1 >> s2; file.close(); qDebug() << s1.Name << "'s ID is " << s1.ID; qDebug() << s2.Name << "'s ID is " << s2.ID; } int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); Save(); Load(); return a.exec(); }
Output:
"Ravel" 's ID is 1 "Schonberg" 's ID is 2
In this section, we'll learn how to overload increment and decrement operators (++ and --). We'll put a focus on the increment operators since the decrement operators are working in the same way. There are two types of increment operators actually: pre-increment(++i) and post-increment(i++).
Note the following:
- v++
(post) increment; this is a postfix expression, and the value of v++ is the value of v before the increment. - ++v
(pre) increment; this is a unary expression, and the value of ++v is the value of v after the increment.
Here is the modified code with the overloaded increment functions.
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&); */ }; 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; }*/ #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; cout << endl; ++c5; cout << "c5= " << c5.getReal() << "+" << c5.getImag() << "i" << endl; c5++; cout << "c5= " << c5.getReal() << "+" << c5.getImag() << "i" << endl; }
The output 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= 10+100i c4= 20+200i c5= 30+300i c5= 31+301i c5= 32+302i
There is one problem with defining both the prefix and postfix opetators: They each take the same number and type of parameters. Normal overloading cannot distinguish between whether the operator we're defining is the prefix version or the postfix.
To solve this problem, the postfix operator function take an extra parameter of the int. When we use the postfix operator, the compiler supplies 0 as the argument for this parameter. Although our postfix function could use this extra parameter, it usually should not. That parameter is not needed for the work normally performed by a postfix operator. Its sole purpose is to distinguish the definition of the postfix function from the prefix version.
Note that the post increment(v++) is returning an old value, and requires local temp variable. This is why we prefer prefix in STL iterators. Also, note that post increment(v++) is returning an object because it's a locally created, and cannot return a reference unlike the pre increment(++v) case which returns reference. Returning a Non-const Object.
The increment(++) and decrement(--) operators are most often implemented for class, such as iterators, that provide pointerlike behavior on the elements of a sequence. In the example below, we define a class that points to an array and provides access to the elements in the array.
#include <iostream> class SmartPtr { public: SmartPtr(int *b, int *e):beg(b), end(e), curr(b) {} SmartPtr operator++(int); SmartPtr& operator++(); int* getCurrent() { return curr; } private: int *beg; int *end; int *curr; }; SmartPtr& SmartPtr::operator++() { if(curr == end) throw "increment past the end"; ++curr; return *this; } SmartPtr SmartPtr::operator++(int) { SmartPtr ret(*this); ++*this; return ret; } int main( ) { using namespace std; int a[] = {1,2,3,4,5}; try { SmartPtr *ptr = new SmartPtr(a,a+5); cout << *(ptr->getCurrent()) << endl; (*ptr)++; cout << *(ptr->getCurrent()) << endl; } catch (const char* e) { cout << "exception: " << e << endl; } return 0; }
Let's look at the postfix version. It is a bit more involved than the prefix operators. They should remember the current state of the object before incrementing the object. This operator defines a local SmartPtr, which is initialized as a copy of *this. In other words, the variable ret is a copy of the current state of this object.
Having kept a copy of the current state, the operator calls its own prefix operator to do the increment:++*this;
calls the SmartPtr prefix increment operator on this object. That operator checks that the increment is safe and either increments curr or throws an exception. If no exception was thrown, the postfix function completes by returning the stored copy in ret. Therefore, after the return, the object itself has been advanced, buit the value returned reflects the original, unincremented value.
SmartPtr& SmartPtr::operator++() { if(curr == end) throw "increment past the end"; ++curr; return *this; }
The implementation of "++" allows this:
aObj++++;while it's not allowed for the built in int:
n++++; // Error
We can see from the output of the following code:
#include <iostream> using namespace std; class A { public: explicit A(int d = 0): mValue(d) {} A operator++(int); friend ostream& operator<<(ostream &os;, const A& a); private: int mValue; }; // this returns temporary stack variable, // so its return type is just A not A& A A::operator++(int) { A temp = *this; mValue++; return temp; } ostream& operator<<(ostream &os;, const A& a) { os << a.mValue << endl; return os; } int main() { int n = 0; A aObj; cout << "n = " << n << " aObj = " << aObj; cout << "n++ = " << n++ << " aObj++ = " << aObj++; cout << "n = " << n << " aObj = " << aObj; cout << "n++ = " << n++ << " aObj++ = " << aObj++; cout << "n = " << n << " aObj = " << aObj; cout << " aObj++++ = " << aObj++++; // cout << "n++++ = " << n++++ << " aObj++ = " << aObj++; cout << "n = " << n << " aObj = " << aObj; return 0; }
Output:
n = 0 aObj = 0 n++ = 0 aObj++ = 0 n = 1 aObj = 1 n++ = 1 aObj++ = 1 n = 2 aObj = 2 aObj++++ = 2 n = 2 aObj = 3
Whenever we overload an operator, we should conform the rule of the operator for the built-in types. But the implementation of the postfix increment does not respect existing behavior of the built-in types.
What went wrong?
When we do:
n++++; // applying postfix increment twiceit's doing this:
n.operator++(0).operator(0);
The second invocation of operator++ is being applied to the object returned from the first one. For the integer it's not allowed because its output would be confusing and against initial intention. The second invocation will end up incrementing the object returned, as a result, even though it's allowed, at the end, it will increment only once which is not the intention of ++++.
So, we need to prohibit users from doing it by returning const object from operator++:
const A A::operator++(int) {...}
To access an element of an array/vector, we need to make [] working. We're going to start from the simplest example as below:
#include <iostream> #include <string> using namespace std; class Vector { int sz; double *elem; public: Vector(int s): sz(s), elem(new double[s]) { for (int i = 0; i < s; i++) elem[i] = 0; } ~Vector() { delete[] elem; } int size() const { return sz; } void set(int n, double val) { elem[n] = val; } double get(int n) { return elem[n]; } }; int main() { Vector v(10); for (int i = 0; i < v.size() ; i++) { v.set(i, i*1.1); } for (int i = 0; i < v.size() ; i++) { cout << v.get(i) << " " << endl; } return 0; }
Here, we use set() and get(), which are kind of working but ugly. Also, we can't not use index operator, []. So, let's modify it a little bit:
#include <iostream> #include <string> using namespace std; class Vector { int sz; double *elem; public: Vector(int s): sz(s), elem(new double[s]) { for (int i = 0; i < s; i++) elem[i] = 0; } ~Vector() { delete[] elem; } int size() const { return sz; } double operator[](int n) { return elem[n]; } }; int main() { Vector v(10); double dval = v[3]; // OK v[4] = 100; // error: '=' : left operand must be l-value return 0; }
In the above example, v[i] is interpreted as v.operator[](i). It returns the value of i-th element of v. However, the v[4] is just a value, not a variable as indicated by the error.
If we modify the overloading [] part, it looks a little bit better:
double *operator[](int n) { return &elem;[n]; } // return a pointer .... int main() { Vector v(10); double dval = *v[3]; // OK *v[4] = 100; // OK cout << "*v[4] = " << *v[4] << endl; //OK return 0; }
But still we need dereference (*) the v to get/set. So, here comes the final version that we can use []:
#include <iostream> #include <string> using namespace std; class Vector { int sz; double *elem; public: Vector(int s): sz(s), elem(new double[s]) { for (int i = 0; i < s; i++) elem[i] = 0; } ~Vector() { delete[] elem; } int size() const { return sz; } double &operator;[](int n) { return elem[n]; } // return reference }; int main() { Vector v(10); double dval = v[3]; // OK v[4] = 100; // OK cout << "v[4] = " << v[4] << endl; //OK return 0; }
Finally, v[i] is interpreted as v.operator[](i), and it returns a reference of the i-th element of v.
The function call operator can be overloaded for objects of class type. The overloaded operator() should be declared as a member function. It is invoked by applying an argument list to an object of the class type.
In the example below, we call the algorithm transform() to apply the operation defined by absValue to every element of the vec.
#include <iostream> #include <vector> #include <algorithm> using namespace std; class absValue { public: int operator()(int val) { return val < 0 ? -val : val; } }; int main() { int a[] = {-3,-2,-1, 0, 1, 2, 3}; int size = sizeof(a)/sizeof(a[0]); vector<int> vec(a, a+size); for(int i = 0; i < size; i++) cout << vec[i] << " "; transform(vec.begin(), vec.end(), vec.begin(), absValue()); cout << "\nafter transform()\n"; for(int i = 0; i < size; i++) cout << vec[i] << " "; return 0; }
Output:
-3 -2 -1 0 1 2 3 after transform() 3 2 1 0 1 2 3
For more detail on the transform, please visit Standard Template Library (STL) V - Function Objects: Predefined Functions Objects.
We can define operators either as members of our class or as non-member functions. Some member must be defined as class members, but others can be defined either way. As an example, let's look at the following code implementing *= operator:
class Complex { public: Complex(double r, double i); Complex *= (const Complex &c;); ... };
But we can use non-member operator:
class Complex { public: Complex(double r, double i); ... }; Complex &operator;*=(Complex &lhs;, const Complex &rhs;);
In the example code below, we defined a new object type, Int. We put the collection object into the list and vector, then we sort the collection. For list element, we used list::sort() while we used std::sort() for vector class.
To sort arbitrary object, we definetely need to overload <(less than). For the vector container, we need additional assignment operator=().
#include <algorithm> #include <iostream> #include <list> #include <vector> using namespace std; class Int { public: Int(int n = 0) : i(n) { } public: bool operator<(const Int& a) const { cout << "operator<" << endl; return this->i < a.i; } Int& operator=(const Int &a;) { cout << "operator=" << endl; this->i = a.i; return *this; } private: int i; }; int main() { list<Int> l; l.push_back(Int(3)); l.push_back(Int(1)); // list::sort(), // it needs customized operator<() cout << "list::sort()" << endl; l.sort(); vector<Int> v; v.push_back(Int(2)); v.push_back(Int()); // std::sort(); // this needs operator=() as well as operator<() cout << endl << "std::sort()" << endl; std::sort(v.begin(), v.end()); return 0; }
Output
list::sort() operator< operator< operator< std::sort() operator< operator< operator= operator=
The following operators should be declared as member methods to ensure that they receive an lvalue as their first operand:
- = assignment
- [] subscript
- -> class member access
- ->* pointer to member selection
- new/delete
- () function call
- (T) conversion (C-style cast)
Operators other than listed above, can be overloaded either as members or as non-members. But in general non-member overloading is recommended. The reasons are as below:
- Symmetry
When a binary operator is defined as a method of a class, it must have an object as the lhs operand. For example, * operator, we should be able to write like complex*10 but not like 10*complex because 10.operator*(complex) does not make any sense. In other words, a*b should be the same as b*a. Otherwise it breaks the commutitiveness that users are expecting from * operator. So, in this case, we should use non-member operator overloading. - Weak coupling
Since a non-member method cannot access private member, it tend to make the class less coupled.
This is from Google C++ Style Guide.
Do not overload operators except in rare, special circumstances.
- Pros
Can make code appear more intuitive because a class will behave in the same way as built-in types (such as int). Overloaded operators are more playful names for functions that are less-colorfully named, such as Equals() or Add(). For some template functions to work correctly, we may need to define operators. - Cons
While operator overloading can make code more intuitive, it has several drawbacks:- It can fool our intuition into thinking that expensive operations are cheap, built-in operations.
- It is much harder to find the call sites for overloaded operators. Searching for Equals() is much easier than searching for relevant invocations of ==.
- Some operators work on pointers too, making it easy to introduce bugs. Foo + 4 may do one thing, while &Foo; + 4 does something totally different. The compiler does not complain for either of these, making this very hard to debug.
- Decision
In general, do not overload operators. The assignment operator (operator=), in particular, is insidious and should be avoided. We can define functions like Equals() and CopyFrom() if we need them. Likewise, avoid the dangerous unary operator& at all costs, if there's any possibility the class might be forward-declared.
However, there may be rare cases where we need to overload an operator to interoperate with templates or standard C++ classes (such as operator<<(ostream&, const T&) for logging). These are acceptable if fully justified, but we should try to avoid these whenever possible. In particular, do not overload operator== or operator< just so that our class can be used as a key in an STL container; instead, we should create equality and comparison functor types when declaring the container.
Some of the STL algorithms do require we to overload operator==, and we may do so in these cases, provided us document why.
More to come...
Ph.D. / Golden Gate Ave, San Francisco / Seoul National Univ / Carnegie Mellon / UC Berkeley / DevOps / Deep Learning / Visualization