A New Assignment Operator, Should You Decide to Accept It - Security - C++ For Dummies (2014)

C++ For Dummies (2014)

Part V

Security

image

image Visit www.dummies.com/extras/cplusplus for great Dummies content online.

In this part…

· Introducing the assignment operator

· Performing input/output

· Handling program errors

· Introducing multiple inheritance

· Applying templates

· Evading hackers

· Visit www.dummies.com/extras/cplusplus for great Dummies content online

Chapter 22

A New Assignment Operator, Should You Decide to Accept It

In This Chapter

arrow Introducing the assignment operator

arrow Knowing why and when the assignment operator is necessary

arrow Understanding similarities between the assignment operator and the copy constructor

arrow Comparing copy semantics with move semantics

The intrinsic data types are built into the language, such as int, float, and double and the various pointer types. Chapters 3 and 4 describe the operators that C++ defines for the intrinsic data types. C++ enables the programmer to define the operators for classes that the programmer has created in addition to these intrinsic operators. This is called operator overloading.

Normally, operator overloading is optional and not attempted by beginning C++ programmers. A lot of experienced C++ programmers (including me) don’t think operator overloading is such a great idea either. However, you will have to learn how to overload one operator: the assignment operator.

Comparing Operators with Functions

An operator is nothing more than a built-in function with a peculiar syntax. The following addition operation

a + b

could be understood as though it were written

operator+(a, b)

In fact, C++ gives each operator a function-style name. The functional name of an operator is the operator symbol preceded by the keyword operator and followed by the appropriate argument types. For example, the + operator that adds an int to an int generating an int is called int operator+(int, int).

Any existing operator can be defined for a user-defined class. Thus, I could create a Complex operator*(const Complex&, const Complex&) that would allow me to multiply two objects of type Complex. The new operator may have the same semantics as the operator it overloads, but it doesn't have to. The following rules apply when overloading operators:

· The programmer cannot overload the . (dot), :: (colon), .*, *->, sizeof and ?: (ternary) operators.

· The programmer cannot invent a new operator. For example, you cannot invent the operation x $ y.

· The syntax of an operator cannot be changed. Thus, you cannot define an operation %i because % is already defined as a binary operator.

· The operator precedence cannot change. A program cannot force operator+ to be evaluated before operator*.

· The operators cannot be redefined when applied to intrinsic types — you can't change the meaning of 1 + 2. Existing operators can be overloaded only for newly defined types.

Overloading operators is one of those things that seems like a much better idea than it really is. In my experience, operator overloading introduces more problems than it solves, with three notable exceptions that are the subject of this chapter.

Inserting a New Operator

The insertion and extraction operators << and >> are nothing more than the left and right shift operators overloaded for a set of input/output classes. These definitions are found in the include file iostream (which is why every program includes that file). Thus, cout << “some string”becomes operator<<(cout, “some string”). Our old friends cout and cin are predefined objects that are tied to the console and keyboard, respectively. I discuss this in detail in Chapter 23.

Creating Shallow Copies Is a Deep Problem

No matter what anyone may think of operator overloading, you’ll need to overload the assignment operator for many classes that you generate. C++ provides a default definition for operator=() for all classes. This default definition performs a member-by-member copy. This works great for an intrinsic type like an int where the only “member” is the integer itself.

int i;
i = 10; // "member by member" copy

This same default definition is applied to user-defined classes. In the following example, each member of source is copied over the corresponding member in destination:

void fn()
{
MyStruct source, destination;
destination = source;
}

The default assignment operator works for most classes; however, it is not correct for classes that allocate resources, such as heap memory. The programmer must overload operator=() to handle the transfer of resources.

The assignment operator is much like the copy constructor (see Chapter 17). In use, the two look almost identical:

void fn(MyClass& mc)
{
MyClass newMC(mc); //of course, this uses the
//copy constructor
MyClass newerMC = mc;//less obvious, this also invokes
//the copy constructor
MyClass newestMC; //this creates a default object
newestMC = mc; //and then overwrites it with
//the argument passed
}

The creation of newMC follows the standard pattern of creating a new object as a mirror image of the original using the copy constructor MyClass(const MyClass&). Not so obvious is that newerMC is also created using the copy constructor. MyClass a = b is just another way of writingMyClass a(b) — in particular, this declaration does not involve the assignment operator despite its appearance. However, newestMC is created using the default constructor and then overwritten with mc using the assignment operator.

image The rule is this: The copy constructor is used when a new object is being created. The assignment operator is used if the left-hand object already exists.

Like the copy constructor, an assignment operator should be provided whenever a shallow copy is not appropriate. (Chapter 17 discusses shallow versus deep copy constructors.) A simple rule is to provide an assignment operator for classes that have a user-defined copy constructor.

Notice that the default copy constructor does work for classes that contain members that themselves have copy constructors, like in the following example:

class Student
{
public:
int nStudentID;
string sName;
};

The C++ library class string does allocate memory off the heap, so the authors of that class include a copy constructor and an assignment operator that (one hopes) perform all the operations necessary to create a successful copy of a string. The default copy constructor for Student invokes the string copy constructor to copy sName from one student to the next. Similarly, the default assignment operator for Student does the same.

Overloading the Assignment Operator

The DemoAssignmentOperator program demonstrates how to provide an assignment operator. The program also includes a copy constructor to provide a comparison:

//DemoAssignmentOperator - demonstrate the assignment
// operator on a user defined class
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;

// DArray - a dynamically sized array class used to
// demonstrate the assignment and copy constructor
// operators
class DArray
{
public:
DArray(int nLengthOfArray = 0)
: nLength(nLengthOfArray), pArray(nullptr)
{
cout << "Creating DArray of length = "
<< nLength << endl;
if (nLength > 0)
{
pArray = new int[nLength];
}
}
DArray(DArray& da)
{
cout << "Copying DArray of length = "
<< da.nLength << endl;
copyDArray(da);
}
~DArray()
{
deleteDArray();
}

//assignment operator
DArray& operator=(const DArray& s)
{
cout << "Assigning source of length = "
<< s.nLength
<< " to target of length = "
<< this->nLength << endl;

//delete existing stuff...
deleteDArray();
//...before replacing with new stuff
copyDArray(s);
//return reference to existing object
return *this;
}

int& operator[](int index)
{
return pArray[index];
}

int size() { return nLength; }

void display(ostream& out)
{
if (nLength > 0)
{
out << pArray[0];
for(int i = 1; i < nLength; i++)
{
out << ", " << pArray[i];
}
}
}

protected:
void copyDArray(const DArray& da);
void deleteDArray();

int nLength;
int* pArray;
};

//copyDArray() - create a copy of a dynamic array of ints
void DArray::copyDArray(const DArray& source)
{
nLength = source.nLength;
pArray = nullptr;
if (nLength > 0)
{
pArray = new int[nLength];
for(int i = 0; i < nLength; i++)
{
pArray[i] = source.pArray[i];
}
}
}

//deleteDArray() - return heap memory
void DArray::deleteDArray()
{
nLength = 0;
delete pArray;
pArray = nullptr;
}

int main(int nNumberofArgs, char* pszArgs[])
{
// a dynamic array and assign it values
DArray da1(5);
for (int i = 0; i < da1.size(); i++)
{
// uses user defined index operator to access
// members of the array
da1[i] = i;
}
cout << "da1="; da1.display(cout); cout << endl;

// now create a copy of this dynamic array using
// copy constructor; this is same as da2(da1)
DArray da2 = da1;
da2[2] = 20; // change a value in the copy
cout << "da2="; da2.display(cout); cout << endl;

// overwrite the existing da2 with the original da1
da2 = da1;
cout << "da2="; da2.display(cout); cout << endl;


// 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, '\n');
cin.get();
return 0;
}

The class DArray defines an integer array of variable length: You tell the class how big an array to create when you construct the object. It does this by wrapping the class around two data members: nLength, which contains the length of the array, and pArray, a pointer to an appropriately sized block of memory allocated off the heap.

The default constructor initializes nLength to the indicated length and then pArray to nullptr.

image The nullptr keyword is new to the '11 standard. If your compiler doesn't recognize nullptr, you can add the following definition near the top of your program:

#define nullptr 0

If the length of the array is actually greater than 0, the constructor allocates an array of int's of the appropriate size off the heap.

The copy constructor creates an array of the same size as the source object and then copies the contents of the source array into the current array using the protected method copyDArray(). The destructor returns the memory allocated in the constructor to the heap using the deleteDArray()method. This method nulls out the pointer pArray once the memory has been deleted.

The assignment operator=() is a method of the class. It looks to all the world like a destructor immediately followed by a copy constructor. This is typical. Consider the assignment in the example da2 = da1. The object da2 already has data associated with it. In the assignment, the original dynamic array must be returned to the heap by calling deleteDArray(), just like the DArray destructor. The assignment operator then invokes copyDArray() to copy the new information into the object, much like the copy constructor.

There are two more details about the assignment operator. First, the return type of operator=() is DArray&, and the returned value is always *this. Expressions involving the assignment operator have a value and a type, both of which are taken from the final value of the left-hand argument. In the following example, the value of operator=() is 2.0, and the type is double.

double d1, d2;
void fn(double);
d1 = 2.0; // the type of this expression is double
// and the value is 2.0

This is what enables the programmer to write the following:

d2 = d1 = 2.0
fn(d2 = 3.0); // performs the assignment and passes the
// resulting value to fn()

The value of the assignment d1 = 2.0 (2.0) and the type (double) are passed to the assignment to d2. In the second example, the value of the assignment d2 = 3.0 is passed to the function fn(), but the type of operator=() is matched to the declarations to find fn(double).

A user-created assignment operator should support the same semantics as the intrinsic version:

fn(DArray&); // given this declaration...
fn(da2 = da1); // ...this should be legal

The second detail is that operator=() was written as a member function. The left-hand argument is taken to be the current object (this). Unlike other operators, the assignment operator cannot be overloaded with a non-member function.

image You can delete the default copy constructor and assignment operator if you don't want to define your own:

class NonCopyable
{
public:
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
};

An object of class NonCopyable cannot be copied via either construction or assignment:

void fn(NonCopyable& src)
{
NonCopyable copy(src); // not allowed
copy = src; // nor is this
}

If your compiler does not support the '11 extensions, you can declare the assignment operator protected:

class NonCopyable
{
protected:
NonCopyable(const NonCopyable&) {};
NonCopyable& operator=(const NonCopyable&)
{return *this};
};

image If your class allocates resources such as memory off the heap, you must make the default assignment operator and copy constructors inaccessible, ideally by replacing them with your own version.

Overloading the Subscript Operator

The earlier DemoAssignmentOperator example program actually slipped in a third operator that is often overloaded for container classes: the subscript operator.

The following definition allows an object of class DArray to be manipulated like an intrinsic array:

int& operator[](int index)
{
return pArray[index];
}

This makes an assignment like the following legal:

int n = da[0]; // becomes n = da.operator[](0);

Notice, however, that rather than return an integer value, the subscript operator returns a reference to the value within pArray. This allows the calling function to modify the value as demonstrated within the DemoAssignmnentOperator program:

da2[2] = 20;

You can see further examples of overloading the index operator for container classes in Chapter 27.

The Move Constructor and Move Operator

image This entire subject is new to C++ '11.

Copy constructors and copy assignment operators are neat for retaining simple semantics for classes that you create. However, since their inception, C++ programmers have not been happy with the inefficiencies that they can create. Consider the following example:

MyContainer fn(int size)
{
MyContainer localMC(size);
return mc;
}

MyContainer mc(fn());

In this case, the function fn() creates a local MyContainer object localMC and then returns it to the caller by value. This simple call could result in the same MyContainer object being copied not once but twice:

1. As part of the return, C++ must make a temporary copy of the localMC object onto the return stack to return to the caller.

2. The subsequent call to the copy constructor copies the contents of this temporary object into the local mc object.

The second copy is unnecessary. Since the temporary object is about to be destructed anyway, the copy constructor could just "take" the assets away from the temporary object rather than go through the hassle of making a copy of something that's about to be put back on the heap anyway. This is the essence of the move constructor.

The move constructor looks like a copy constructor except for two things:

· A move constructor takes the resources from the source and gives them to the target rather than copying.

· The argument of the move constructor is of type MyContainer&&, the double ampersand meaning “only use for temporary values.”

The following example program shows both the move constructor and move assignment operator in action:

// DemoMoveOperator - demonstrate the move operator
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <cstring>

using namespace std;
class MyContainer
{
public:
MyContainer(int nS, const char* pS) : nSize(nS)
{
pString = new char[nSize];
strcpy(pString, pS);
}
~MyContainer()
{
delete pString;
pString = nullptr;
}

//copy constructor
MyContainer(const MyContainer& s)
{
copyIt(*this, s);
}
MyContainer& operator=(MyContainer& s)
{
delete pString;
copyIt(*this, s);
return *this;
}

// move constructor
MyContainer(MyContainer&& s)
{
moveIt(*this, s);
}
MyContainer& operator=(MyContainer&& s)
{
delete pString;
moveIt(*this, s);
return *this;
}

protected:
static void moveIt(MyContainer& tgt, MyContainer& src)
{
cout << "Moving " << src.pString << endl;
tgt.nSize = src.nSize;
tgt.pString = src.pString;
src.nSize = 0;
src.pString = nullptr;
}
static void copyIt( MyContainer& tgt,
const MyContainer& src)
{
cout << "Copying " << src.pString << endl;
delete tgt.pString;
tgt.nSize = src.nSize;
tgt.pString = new char[tgt.nSize];
strncpy(tgt.pString, src.pString, tgt.nSize);
}
int nSize;
char* pString;
};

MyContainer fn(int n, const char* pString)
{
MyContainer b(n, pString);
return b;
}

int main(int nNumberofArgs, char* pszArgs[])
{
MyContainer mc(100, "Original");

mc = fn(100, "Created in fn()");

// 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, '\n');
cin.get();
return 0;
}

The output from this program appears as follows:

Moving Created in fn()
Press Enter to continue...

The function fn() returns a temporary object that is moved over into the mc object using the move assignment operator, operator=(MyContainer&&). The moveIt() function is a lot faster to execute than the copyIt() function would have been — it doesn't allocate memory off of the heap or copy anything. The moveIt() function simply takes the memory block from the src object which, in this case, is the temporary returned from fn().

image Make sure that you zero out the pointer in the src object; otherwise, the destructor will return the memory block to the heap, leaving the target object pointing to unallocated memory