HiveBrain v1.2.0
Get Started
← Back to all entries
patterncppCritical

What is The Rule of Three?

Submitted by: @import:stackoverflow-api··
0
Viewed 0 times
whatrulethreethe

Problem


  • What does copying an object mean?



  • What are the copy constructor and the copy assignment operator?



  • When do I need to declare them myself?



  • How can I prevent my objects from being copied?

Solution

Introduction

C++ treats variables of user-defined types with value semantics.
This means that objects are implicitly copied in various contexts,
and we should understand what "copying an object" actually means.

Let us consider a simple example:

class person
{
    std::string name;
    int age;

public:

    person(const std::string& name, int age) : name(name), age(age)
    {
    }
};

int main()
{
    person a("Bjarne Stroustrup", 60);
    person b(a);   // What happens here?
    b = a;         // And here?
}


(If you are puzzled by the name(name), age(age) part,
this is called a member initializer list.)
Special member functions

What does it mean to copy a person object?
The main function shows two distinct copying scenarios.
The initialization person b(a); is performed by the copy constructor.
Its job is to construct a fresh object based on the state of an existing object.
The assignment b = a is performed by the copy assignment operator.
Its job is generally a little more complicated
because the target object is already in some valid state that needs to be dealt with.

Since we declared neither the copy constructor nor the assignment operator (nor the destructor) ourselves,
these are implicitly defined for us. Quote from the standard:

The [...] copy constructor and copy assignment operator, [...] and destructor are special member functions.
[ Note: The implementation will implicitly declare these member functions
for some class types when the program does not explicitly declare them.
The implementation will implicitly define them if they are used. [...] end note ]
[n3126.pdf section 12 §1]

By default, copying an object means copying its members:

The implicitly-defined copy constructor for a non-union class X performs a memberwise copy of its subobjects.
[n3126.pdf section 12.8 §16]

The implicitly-defined copy assignment operator for a non-union class X performs memberwise copy assignment
of its subobjects.
[n3126.pdf section 12.8 §30]

Implicit definitions

The implicitly-defined special member functions for person look like this:

// 1. copy constructor
person(const person& that) : name(that.name), age(that.age)
{
}

// 2. copy assignment operator
person& operator=(const person& that)
{
    name = that.name;
    age = that.age;
    return *this;
}

// 3. destructor
~person()
{
}


Memberwise copying is exactly what we want in this case:
name and age are copied, so we get a self-contained, independent person object.
The implicitly-defined destructor is always empty.
This is also fine in this case since we did not acquire any resources in the constructor.
The members' destructors are implicitly called after the person destructor is finished:

After executing the body of the destructor and destroying any automatic objects allocated within the body,
a destructor for class X calls the destructors for X's direct [...] members
[n3126.pdf 12.4 §6]

Managing resources

So when should we declare those special member functions explicitly?
When our class manages a resource, that is,
when an object of the class is responsible for that resource.
That usually means the resource is acquired in the constructor
(or passed into the constructor) and released in the destructor.

Let us go back in time to pre-standard C++.
There was no such thing as std::string, and programmers were in love with pointers.
The person class might have looked like this:

class person
{
    char* name;
    int age;

public:

    // the constructor acquires a resource:
    // in this case, dynamic memory obtained via new[]
    person(const char* the_name, int the_age)
    {
        name = new char[strlen(the_name) + 1];
        strcpy(name, the_name);
        age = the_age;
    }

    // the destructor must release this resource via delete[]
    ~person()
    {
        delete[] name;
    }
};


Even today, people still write classes in this style and get into trouble:
"I pushed a person into a vector and now I get crazy memory errors!"
Remember that by default, copying an object means copying its members,
but copying the name member merely copies a pointer, not the character array it points to!
This has several unpleasant effects:

  • Changes via a can be observed via b.



  • Once b is destroyed, a.name is a dangling pointer.



  • If a is destroyed, deleting the dangling pointer yields undefined behavior.



  • Since the assignment does not take into account what name pointed to before the assignment,


sooner or later you will get memory leaks all over the place.

Explicit definitions

Since memberwise copying does not have the desired effect, we must define the copy constructor and the copy assignment operator explicitly to make deep copies of the character array:

```
// 1. copy constructor
person(const person& that)
{
name = new char[strlen(that.name) + 1];
strcpy(name, that.name);
age = that.age;
}

// 2. copy assignment operator
person& operator=(const person& that)
{

Code Snippets

class person
{
    std::string name;
    int age;

public:

    person(const std::string& name, int age) : name(name), age(age)
    {
    }
};

int main()
{
    person a("Bjarne Stroustrup", 60);
    person b(a);   // What happens here?
    b = a;         // And here?
}
// 1. copy constructor
person(const person& that) : name(that.name), age(that.age)
{
}

// 2. copy assignment operator
person& operator=(const person& that)
{
    name = that.name;
    age = that.age;
    return *this;
}

// 3. destructor
~person()
{
}
class person
{
    char* name;
    int age;

public:

    // the constructor acquires a resource:
    // in this case, dynamic memory obtained via new[]
    person(const char* the_name, int the_age)
    {
        name = new char[strlen(the_name) + 1];
        strcpy(name, the_name);
        age = the_age;
    }

    // the destructor must release this resource via delete[]
    ~person()
    {
        delete[] name;
    }
};
// 1. copy constructor
person(const person& that)
{
    name = new char[strlen(that.name) + 1];
    strcpy(name, that.name);
    age = that.age;
}

// 2. copy assignment operator
person& operator=(const person& that)
{
    if (this != &that)
    {
        delete[] name;
        // This is a dangerous point in the flow of execution!
        // We have temporarily invalidated the class invariants,
        // and the next statement might throw an exception,
        // leaving the object in an invalid state :(
        name = new char[strlen(that.name) + 1];
        strcpy(name, that.name);
        age = that.age;
    }
    return *this;
}
// 2. copy assignment operator
person& operator=(const person& that)
{
    char* local_name = new char[strlen(that.name) + 1];
    // If the above statement throws,
    // the object is still in the same state as before.
    // None of the following statements will throw an exception :)
    strcpy(local_name, that.name);
    delete[] name;
    name = local_name;
    age = that.age;
    return *this;
}

Context

Stack Overflow Q#4172722, score: 2161

Revisions (0)

No revisions yet.