Chapter 22. A New Assignment Operator, Should You Decide to Accept It

In This Chapter

  • Introducing the assignment operator

  • Knowing why and when the assignment operator is necessary

  • Understanding similarities between the assignment operator and the copy constructor

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 must figure out 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*(Complex&, 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), .*, and ?: (ternary) operators.

  • The programmer cannot invent new operators. For example, you cannot invent the operation x $ y.

  • The format of the operators 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(MyClass&). Not so obvious is that newerMC is also created using the copy constructor. MyClass a = b is just another way of writing MyClass 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.

Note

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>
// the following is necessary if your compiler doesn't
// understand the C++ '09 keyword 'nullptr'
#define nullptr 0
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 out(const char* pszName)
    {
        cout << pszName << ": ";
        int i = 0;
        while (true)
        {
            cout << pArray[i];
            if (++i >= nLength)
            {
                break;
            }
            cout << ", ";
        }
        cout << endl;
    }

  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;
    }
    da1.out("da1");

    // 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
    da2.out("da2");

    // overwrite the existing da2 with the original da1
    da2 = da1;
    da2.out("da2");


    // wait until user is ready before terminating program
    // to allow the user to see the program results
    system("PAUSE");
    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.

Note

The nullptr keyword is new to the '09 standard. If your compiler doesn't recognize nullptr, you can add the following definition near the top of your program, as I have done here:

#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().

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 nonmember function.

Note

You can delete the default 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 this '09 extension, you can declare the assignment operator protected:

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

Tip

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 to 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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.222.185