11.6. operator=

Time: 00:03:22 | Download: Large, Large (CC), Small | Streaming, Streaming (CC) | Slides (PDF)

Review

Assigning one object to another is a fundamental operation, so the compiler automatically creates an overloaded assignment operator. The compiler-created operator is simple and based on a general algorithm that copies each field or member variable of the original object into the memory allocated for the other object. The following figure illustrates two possible implementations, but they only work with simple classes without pointer members.

Class Specification Member-By-Member Copy Byte-By-Byte Copy
class Person
{
    private:
        int	id;
        int     weight;
        double  height;
};
 
 
 
Person& Person::operator=(Person& p)
{
    if (this == &p)		// (a)
        return *this;
    id = p.id;			// (b)
    weight = p.weight;
    height = p.height;

    return *this;		// (c)
}
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 assignment operator is similar to but more complex than the copy constructor. The complicating difference is the copy constructor is making a new object, while the assignment operator is dealing with an existing object (the one on the left side of the operator). Assuming that p is an instance of the Person, the statement Person p2 = p; seems to illustrate a situation where the left-hand operand doesn't exist before the copy. Surprisingly, the statement uses the copy constructor rather than the assignment operator.
  1. The if-statement prevents copying an object over itself (e.g., p = p). The test increases efficiency, and if the compiler-generated operator uses memcpy, it protects it because many implementations cannot handle overlapping memory locations.
  2. An assignment operator can perform a member-by-member copy with a series of "regular" (with fundamental-type operands) assignment operations, or it can perform a byte-by-byte copy with the memcpy function.
  3. The object copy occurs in the function's body, independent of the returned value. However, the unusual return value allows programs to chain assignment operations. For example, if p1, p2, and p3 are instances of the Person class, assignment chaining is a statement with two or more assignment operations: 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 cases but fails in others. First, we must learn when the compiler-created operator works so we don't needlessly override it. But then we must also know when the compiler fails so we can override it with a 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 they point to. 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" but not "complex" objects. (Please see the previous working definitions of "simple" and "complex" classes.)
  1. The compiler-created assignment operator copies instances of simple classes completely. Simple objects embed all their data within their boundaries, and the assignment operation duplicates the data in the receiving (i.e., left-hand) object.
    int	id;
    int	weight;
    double	height;
  2. Complex objects point to one or more data items existing outside their boundaries. The compiler-created assignment operator copies all the original object's data, including the addresses stored in the pointer members, but it does not copy the external objects. The result is two whole objects sharing a single part.
    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, allowing the program to change one object without affecting the other. However, the original and copied objects continue to share the name member, and changing the name in one object changes it in the other.

There may be situations where allowing two objects to share data is convenient or efficient. However, programmers typically intend the assignment operation to result in independent objects, forcing programmers to overload the assignment operators for complex classes with pointer members.

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 the function uses memcpy.
  2. If the left-hand object has a pointer to heap memory allocated with the new operator, the assignment operator must destroy it to avoid a memory leak. (Note: this step only applies if the left-hand object "owns" the data.)
  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 and copy the existing data to the newly allocated memory.
  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 fully independent after the copy operation finishes.
(c)
Overloaded assignment operator for complex objects. 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 significant - doing the memcpy last will overwrite the addresses in any pointer variables.
  3. A complete object copy: The overloaded operator correctly copies the pointer data using the new operator and string copy constructor, resulting in independent objects.
  1. Tests for self assignment (e.g., A = A), returning early if it is detected.
  2. Test for and delete existing data before replacing it with new data.
  3. Copies the member variables.
  4. Returns a reference to the left-hand object, allowing assignment chaining.
Although the assignment operator and copy constructor have step iii in common, the copy constructor does not include steps i, ii, or iv. The difference between the functions is that the copy constructor creates a new object. In contrast, the assignment operator's left-hand object already exists, and the program may have used it previously, assigning values to the pointer members.