11.6. operator=

Review

Assigning one object to another is a fundamental operation, which means that it is both an import operation and handled automatically by the compiler - the compiler automatically creates an overloaded assignment operator function. The compiler-created function is simple and based on a general algorithm that performs a byte-wise copy of the original object into the memory allocated for the second object. The next figure illustrates a possible implementation.

class person
{
    private:
        string  name;
        int     weight;
        double  height;
};
person& person::operator=(person& p)
{
    if (this == &p)			// (a)
        return *this;
    memcpy(this, &p, sizeof(person));	// (b)
    return *this;			// (c)
}
The compiler-generated assignment operator. The the assignment operator is very similar to the copy constructor, but the assignment operator's task is more complicated. The complicating difference is that the copy constructor is making a new object while the assignment operator is dealing with an exiting object (the one on the left side of the operator).
  1. The if-statement prevents copying an object over itself (e.g., p = p). The test not only increases efficiency but also protects memcpy, which, for many implementations, does not handle overlapping memory locations.
  2. The memory copy function (memcpy) does a byte-by-byte copy of data from one memory location to another.
  3. The object copy takes place in the body of the function, and the return value is only needed to allow chaining assignment operations. If p1, p2, and p3 be instances of the person class, then a simple example of an assignment chain is: p3 = p2 = p1;. The assignment operator is right associative (i.e., is evaluated right to left), so the operation p2 = p1 takes place first and the returned value becomes the right hand operand for the left-most assignment operation. The function return type is not a pointer, so the this pointer is dereferenced.

Overloading operator= (The Assignment Operator)

The complier-created assignment operator works in some but not all cases. First, we must learn when the compiler-created operator works so that we don't needlessly override it. But then we must also know when the compiler fails so that we can override it with correct implementation.

Simple Class Complex Class
The compiler-generated copy constructor copies an object byte-by-byte to a new object. This method is simple, efficient, and works well for 'simple' classes (classes without pointer members). When a class has one or more pointer members, the compiler-generated copy constructor still copies the original object, including the pointer members, byte-by-byte. So, the compiler-generated copy constructor copies the addresses stored in the pointers, not the objects to which they point. The the compiler-generated copy constructor implements an incomplete copy, leaving the original and the new object sharing all aggregated part objects.
(a)(b)
The compiler-created assignment operator function. The compiler-created assignment operator works well when copying simple objects but not complex ones. (Please see the previous definitions of "simple" and "complex" classes.)
  1. The compiler-created assignment operator copies instances of simple classes completely. All of the data belonging to the original object is contained within the object is copied with the memcpy function.
    string	name;
    int	weight;
    double	height;
  2. The compiler-created assignment operator does copy all of the original object's data - but it does not copy data that lies outside of the object - it copies the address stored in the pointer variable but it does not copy any data that is the target of the pointer variable.
    string*  name;
    int	 weight;
    double	 height;
    Following the copy, the values stored in the weight and height member variables of the two objects are independent and can change between the objects. However, the original and copied objects continue to share the member variable name, and changing the name in one object changes it in the other.

There may be situations where it is convenient or efficient to have two different objects share data. But usually, when we copy an object, we most often intend to copy it completely. When dealing with complex objects, achieving a complete copy requires that we replace the complier-created assignment operator.

Steps for overloading the assignment operator

  1. Test for self assignment: p1 == p1. This step improves the efficiency of the operation by preventing a needless copy and is necessary if memcpy is used.
  2. If the left hand object already exists and if the pointer members point to memory allocated with the new operator, the to avoid a memory leak, the previously allocated memory must be returned to the free store (i.e., the heap) by deleting it. (Note: this step only applies if the left hand object "owns" the data that is the target of the pointer.)
  3. Copy each non-pointer member variable by simple assignment or by using memcpy.
  4. Copy each pointer member by allocating new memory with the new operator.
  5. Return a reference to the left hand operand (see Figure 1).
person& person::operator=(person& p)
{
    if (&p == this)			// i
	return *this;

    if (name != nullptr) delete name;	// ii

    name = new string(*p.name);		// iii
    weight = p.weight;
    height = p.height;

    return *this;			// iv
}
person& person::operator=(person& p)
{
    if (&p == this)			// i
	return *this;

    if (name != nullptr)		// ii
        delete name;

    memcpy(this, &p, sizeof(person));	// iii
    name = new string(*p.name);

    return *this;			// iv
}
(a)(b)
A correctly overloaded copy constructor performs a complete copy. It copies all aggregated part objects so that the original and new objects are fulling independent after the copy operation finishes.
(c)
Overloaded assignment operator with pointers. Two options for overloading operator= (the assignment operator).
  1. Member-by-member copy using the fundamental or built-in assignment operator - a common way of overloading the assignment operator.
  2. Byte-wise copy followed by a member-by-member copy of pointer members. Notice that the order is very important - doing the memcpy last will overwrite the addresses in any pointer variables.
  3. A fully and correctly copied object - the data that is the target of pointer members is correctly copied using the "new" operator. The result is that the two objects are distinct and do not share any data.
  1. Tests for assigning an object to itself (e.g., A = A); returns early, skipping the copy operations, in the case of self-assignment
  2. Deletes existing data before replacing it with new data
  3. Copies the member variables
  4. Returns a reference to the left hand operand (i.e., the left hand object), which allows assignment chaining

The overloaded assignment operator has much in common with the overloaded copy constructor. However, the copy constructor does not include steps i, ii, or iv. Why does the assignment operator require these steps but the constructor does not? The simple answer is that the copy constructor is creating a new object while the object on the left-hand side of the assignment operator is not new and may contain data.

More specifically, if the left-hand object was used previously and already points to exiting data, overwriting the address stored in the pointer can result in a memory leak. Additionally, the memcpy library function, if used to implement the overloaded assignment operator, cannot copy overlapping memory locations.