Chapter 30
In This Chapter
Overloading operators — in general, a bad idea
Overloading the assignment operator — why that one is critical
Getting by without an assignment operator
The little symbols like +, −, =, and so on are called operators. These operators are already defined for the intrinsic types like int and double. However, C++ allows you to define the existing operators for classes that you create. This is called operator overloading.
Operator overloading sounds like a great idea. The examples that are commonly named are classes such as Complex that represent complex numbers. (Don’t worry if you don’t know what a complex number is. Just know that C++ doesn’t handle them intrinsically.) Having defined the class Complex, you can then define the addition, multiplication, subtraction, and division operators (all of these operations are defined for complex numbers). Then you write cool stuff like this:
Complex c1(1, 0), c2(0, 1);
Complex c3 = c1 + c2;
Whoa, there, not so fast. Overloading operators turns out to be much more difficult in practice than in theory. So much so that I consider operator overloading beyond the scope of this book, with two exceptions — one of which is the subject of this chapter: overloading the assignment operator. The second operator worth overloading is the subject of the next chapter. But first things first …
C++ considers an overloaded operator as a special case of a function call. It considers the + operator to be shorthand for the function operator+(). In fact, for any operator %, the function version is known as operator%(). So to define what addition means when applied to a Complex object, for example, you need merely to define the following function:
Complex& operator+(const Complex& c1, const Complex& c2);
You can define what existing operators mean when applied to objects of your making, but there are a lot of things you can’t do when overloading operators. Here are just a few:
In addition, the assignment operator must be a member function — it cannot be a non-member function like the addition operator just defined. (For more about member functions, see Chapter 22.)
The C++ language does provide an assignment operator. That’s why you can write things like the following:
Student s1("Stephen Davis", 1234);
Student s2;
s2 = s1; // use the default assignment operator
The C++ provided assignment operator does a member-by-member copy of each data member from the object on the right into the object on the left using each data member’s assignment operator. This is completely analogous to the C++ provided copy constructor. Remember that this member-by-member copy is called a shallow copy. (Refer to Chapter 27 for more on copy constructors and shallow copies.)
The problems inherent in the C++ provided assignment operator are similar to those of the copy constructor, only worse. Consider the following example snippet:
class Student
{
protected:
char* pszName;
int nID;
public:
Student(const char* pszNewName, int nNewID)
{
cout << "Constructing " << pszNewName << endl;
int nLength = strlen(pszNewName) + 1;
pszName = new char[nLength];
strcpy(pszName, pszNewName);
nID = nNewID;
}
~Student()
{
cout << "Destructing " << pszName << endl;
delete[] pszName;
pszName = nullptr;
}
// ...other members...
};
void someFn()
{
Student s1("Stephen Davis", 1234);
Student s2("Cayden Amrich", 5678);
s2 = s1; // this is legal but very bad
}
The function someFn() first creates an object s1. The Student(const char*, int) constructor for Student allocates memory from the heap to use to store the student’s name. The process is repeated for s2.
The function then assigns s1 to s2. This does two things, both of which are bad:
Here’s what the assignment operator for Student needs to do:
The following StudentAssignment program contains a Student class that has a constructor and a destructor along with a copy constructor and an assignment operator — everything a self-respecting class needs!
//
// StudentAssignment - this program demonstrates how to
// create an assignment operator that
// performs the same deep copy as the copy
// constructor
//
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <cstring>
using namespace std;
class Student
{
protected:
char* pszName;
int nID;
void init(const char* pszNewName, int nNewID)
{
int nLength = strlen(pszNewName) + 1;
pszName = new char[nLength];
strcpy(pszName, pszNewName);
nID = nNewID;
}
void destruct()
{
delete[] pszName;
pszName = nullptr;
}
public:
Student(const char* pszNewName, int nNewID)
{
cout << "Constructing " << pszNewName << endl;
init(pszNewName, nNewID);
}
Student(Student& s)
{
cout<<"Constructing copy of "<< s.pszName << endl;
init(s.pszName, s.nID);
}
virtual ~Student()
{
cout << "Destructing " << pszName << endl;
destruct();
}
// overload the assignment operator
Student& operator=(const Student& source)
{
// don't do anything if we are assigned to
// ourselves
if (this != &source)
{
cout << "Assigning " << source.pszName
<< " to " << pszName << endl;
// first destruct the existing object
destruct();
// now copy the source object
init(source.pszName, source.nID);
}
return *this;
}
// access functions
const char* getName()
{
return pszName;
}
int getID()
{
return nID;
}
};
void someFn()
{
Student s1("Adam Laskowski", 1234);
Student s2("Vanessa Barbossa", 5678);
s2 = s1;
}
int main(int nNumberofArgs, char* pszArgs[])
{
someFn();
// wait until user is ready before terminating program
// to allow the user to see the program results
cout << "Press Enter to continue..." << endl;
cin.ignore(10, '
'),
cin.get();
return 0;
}
The data members of this Student class are the same as the versions from earlier chapters. The constructor and copy constructor are the same as well, except that the actual work is performed in an init() function invoked from both constructors. The assignment operator can reuse the same init() function as well to perform its construction function.
The code that implements the destruct sequence has also been transferred from ~Student() to a protected destruct() member function.
Following the destructor is the assignment operator operator=(). This function first tests to see if the address of the object passed is the same as the current object. This is to detect the following case:
s1 = s1;
In this case, the assignment operator does nothing. If the source and current objects are not the same, the function first destructs the current object and then copies the contents of the source object into the current object. Finally, it returns a reference to the current object.
The someFn() function shows how this works in practice. After first declaring two Student objects s1 and s2, someFn() executes the assignment
s2 = s1;
which is interpreted as if it had been written as
s2.operator=(s1);
That is, the assignment operator destructs s2 and then deep-copies the contents of s1 into s2.
The destructor invoked at the end of someFn() demonstrates that the two objects, s1 and s2, don’t both refer to the same piece of heap memory. The output from the program appears as follows:
Constructing Adam Laskowski
Constructing Vanessa Barbossa
Assigning Adam Laskowski to Vanessa Barbossa
Destructing Adam Laskowski
Destructing Adam Laskowski
Press Enter to continue …
s3 = s1 = s2;
I don’t expect you to learn all the ins and outs of overloading operators; however, you can’t go too wrong if you follow the pattern set out by the Student example:
If all this is too much, you can use the delete keyword to delete the default assignment operator, like so:
class Student
{
public:
Student& operator=(const Student&) = delete;
// ...whatever else...
};
This command removes the default assignment operator without replacing it with a user-defined version. Without an assignment operator, the assignment
s1 = s2;
generates a compiler error.
18.222.182.66