Overload Assignment Operator C++ Template Example

The assignment operator (operator=) is used to copy values from one object to another already existing object.

Assignment vs Copy constructor

The purpose of the copy constructor and the assignment operator are almost equivalent -- both copy one object to another. However, the copy constructor initializes new objects, whereas the assignment operator replaces the contents of existing objects.

The difference between the copy constructor and the assignment operator causes a lot of confusion for new programmers, but it’s really not all that difficult. Summarizing:

  • If a new object has to be created before the copying can occur, the copy constructor is used (note: this includes passing or returning objects by value).
  • If a new object does not have to be created before the copying can occur, the assignment operator is used.

Overloading the assignment operator

Overloading the assignment operator (operator=) is fairly straightforward, with one specific caveat that we’ll get to. The assignment operator must be overloaded as a member function.

This prints:

5/3

This should all be pretty straightforward by now. Our overloaded operator= returns *this, so that we can chain multiple assignments together:

Issues due to self-assignment

Here’s where things start to get a little more interesting. C++ allows self-assignment:

This will call f1.operator=(f1), and under the simplistic implementation above, all of the members will be assigned to themselves. In this particular example, the self-assignment causes each member to be assigned to itself, which has no overall impact, other than wasting time. In most cases, a self-assignment doesn’t need to do anything at all!

However, in cases where an assignment operator needs to dynamically assign memory, self-assignment can actually be dangerous:

First, run the program as it is. You’ll see that the program prints “Alex” as it should.

Now run the following program:

You’ll probably get garbage output (or a crash). What happened?

Consider what happens in the overloaded operator= when the implicit object AND the passed in parameter (str) are both variable alex. In this case, m_data is the same as str._m_data. The first thing that happens is that the function checks to see if the implicit object already has a string. If so, it needs to delete it, so we don’t end up with a memory leak. In this case, m_data is allocated, so the function deletes m_data. But str.m_data is pointing to the same address! This means that str.m_data is now a dangling pointer.

Later on, when we’re copying the data from str into our implicit object, we’re accessing dangling pointer str.m_data. That leaves us either copying garbage data or trying to access memory that our application no longer owns (crash).

Detecting and handling self-assignment

Fortunately, we can detect when self-assignment occurs. Here’s a better implementation of our overloaded operator= for the Fraction class:

By checking if our implicit object is the same as the one being passed in as a parameter, we can have our assignment operator just return immediately without doing any other work.

Note that there is no need to check for self-assignment in a copy-constructor. This is because the copy constructor is only called when new objects are being constructed, and there is no way to assign a newly created object to itself in a way that calls to copy constructor.

Default assignment operator

Unlike other operators, the compiler will provide a default public assignment operator for your class if you do not provide one. This assignment operator does memberwise assignment (which is essentially the same as the memberwise initialization that default copy constructors do).

Just like other constructors and operators, you can prevent assignments from being made by making your assignment operator private or using the delete keyword:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

#include <cassert>

#include <iostream>

 

classFraction

{

private:

intm_numerator;

intm_denominator;

 

public:

    // Default constructor

    Fraction(intnumerator=0,intdenominator=1):

        m_numerator(numerator),m_denominator(denominator)

    {

        assert(denominator!=0);

    }

 

// Copy constructor

Fraction(constFraction&copy):

m_numerator(copy.m_numerator),m_denominator(copy.m_denominator)

{

// no need to check for a denominator of 0 here since copy must already be a valid Fraction

std::cout<<"Copy constructor called\n";// just to prove it works

}

 

        // Overloaded assignment

        Fraction&operator=(constFraction&fraction);

 

friendstd::ostream&operator<<(std::ostream&out,constFraction&f1);

        

};

 

std::ostream&operator<<(std::ostream&out,constFraction&f1)

{

out<<f1.m_numerator<<"/"<<f1.m_denominator;

returnout;

}

 

// A simplistic implementation of operator= (see better implementation below)

Fraction&Fraction::operator=(constFraction&fraction)

{

    // do the copy

    m_numerator=fraction.m_numerator;

    m_denominator=fraction.m_denominator;

 

    // return the existing object so we can chain this operator

    return*this;

}

 

intmain()

{

    Fraction fiveThirds(5,3);

    Fractionf;

    f=fiveThirds;// calls overloaded assignment

    std::cout<<f;

 

    return0;

}

intmain()

{

    Fraction f1(5,3);

    Fraction f2(7,2);

    Fraction f3(9,5);

 

    f1=f2=f3;// chained assignment

 

    return0;

}

intmain()

{

    Fraction f1(5,3);

    f1=f1;// self assignment

 

    return0;

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

#include <iostream>

 

classMyString

{

private:

    char*m_data;

    intm_length;

 

public:

    MyString(constchar*data="",intlength=0):

        m_length(length)

    {

        if(!length)

            m_data=nullptr;

        else

            m_data=newchar[length];

 

        for(inti=0;i<length;++i)

            m_data[i]=data[i];

    }

 

    // Overloaded assignment

    MyString&operator=(constMyString&str);

 

    friendstd::ostream&operator<<(std::ostream&out,constMyString&s);

};

 

std::ostream&operator<<(std::ostream&out,constMyString&s)

{

    out<<s.m_data;

    returnout;

}

 

// A simplistic implementation of operator= (do not use)

MyString&MyString::operator=(constMyString&str)

{

    // if data exists in the current string, delete it

    if(m_data)delete[]m_data;

 

    m_length=str.m_length;

 

    // copy the data from str to the implicit object

    m_data=newchar[str.m_length];

 

    for(inti=0;i<str.m_length;++i)

        m_data[i]=str.m_data[i];

 

    // return the existing object so we can chain this operator

    return*this;

}

 

intmain()

{

    MyString alex("Alex",5);// Meet Alex

    MyString employee;

    employee=alex;// Alex is our newest employee

    std::cout<<employee;// Say your name, employee

 

    return0;

}

intmain()

{

    MyString alex("Alex",5);// Meet Alex

    alex=alex;// Alex is himself

    std::cout<<alex;// Say your name, Alex

 

    return0;

}

// A better implementation of operator=

Fraction&Fraction::operator=(constFraction&fraction)

{

    // self-assignment guard

    if(this==&fraction)

        return*this;

 

    // do the copy

    m_numerator=fraction.m_numerator;

    m_denominator=fraction.m_denominator;

 

    // return the existing object so we can chain this operator

    return*this;

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

#include <cassert>

#include <iostream>

 

classFraction

{

private:

intm_numerator;

intm_denominator;

 

public:

    // Default constructor

    Fraction(intnumerator=0,intdenominator=1):

        m_numerator(numerator),m_denominator(denominator)

    {

        assert(denominator!=0);

    }

 

// Copy constructor

Fraction(constFraction&copy)=delete;

 

// Overloaded assignment

Fraction&operator=(constFraction&fraction)=delete;// no copies through assignment!

 

friendstd::ostream&operator<<(std::ostream&out,constFraction&f1);

        

};

 

std::ostream&operator<<(std::ostream&out,constFraction&f1)

{

out<<f1.m_numerator<<"/"<<f1.m_denominator;

returnout;

}

 

intmain()

{

    Fraction fiveThirds(5,3);

    Fractionf;

    f=fiveThirds;// compile error, operator= has been deleted

    std::cout<<f;

 

    return0;

}

C++ Tutorial Operator Overloading i - 2018




bogotobogo.com site search:





Functions


Default Arguments

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

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:

  1. in the number of arguments
  2. or in the type of arguments
  3. 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


Operator Overloading

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.



Operators that cannot be overloaded

The following operators cannot be overloaded:

  1. . member selection
  2. .* member selection with pointer-to-member
  3. ?: conditional
  4. :: scope resolution
  5. # stringizing operator
  6. ## merging operator
  7. sizeof object size information
  8. typeid object type information


Overloading '='

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




Overloading '+'

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

Overloading '+' using Friend Function

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:

  1. Although the operator+() function is not a member function, it has the same access rights as a member function.
  2. 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

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 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);




Class Serialization - Overloading (<<) and (>>) Operators

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 &in;, 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

Increment/Decrement '++/--' Operator Overloading

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:

  1. v++
    (post) increment; this is a postfix expression, and the value of v++ is the value of v before the increment.
  2. ++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; }




How about "++++"

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 twice it'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) {...}

Index Operator [] Overloading

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.



Function Call Operator () Overloading

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.



Member operator vs non-member operator

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;);

Operator overloading for a user defined object type

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=




Operators that should be declared as member methods

The following operators should be declared as member methods to ensure that they receive an lvalue as their first operand:

  1. = assignment
  2. [] subscript
  3. -> class member access
  4. ->* pointer to member selection
  5. new/delete
  6. () function call
  7. (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:

  1. 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.
  2. Weak coupling
    Since a non-member method cannot access private member, it tend to make the class less coupled.

Operator overloading - pros and cons

This is from Google C++ Style Guide.

Do not overload operators except in rare, special circumstances.

  1. 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.
  2. Cons
    While operator overloading can make code more intuitive, it has several drawbacks:
    1. It can fool our intuition into thinking that expensive operations are cheap, built-in operations.
    2. It is much harder to find the call sites for overloaded operators. Searching for Equals() is much easier than searching for relevant invocations of ==.
    3. 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.
    Overloading also has surprising ramifications. For instance, if a class overloads unary operator&, it cannot safely be forward-declared.
  3. 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


Sponsor Open Source development activities and free contents for everyone.

Thank you.

- K Hong



0 Replies to “Overload Assignment Operator C++ Template Example”

Lascia un Commento

L'indirizzo email non verrà pubblicato. I campi obbligatori sono contrassegnati *