Chapter 13. Copy Control

A class controls these operations by defining five special member functions: copy constructor, copy-assignment operator, move constructor, move-assignment operator, and destructor. The copy and move constructors define what happens when an object is initialised from another object of the same type. The copy- and move-assignment operators define what happens when we assign an object of a class type to another object of that same class type. The destructor defines what happens when an object of the type ceases to exist. Collectively, we will refer to these operations as copy control.

13.1 Copy, Assign, and Destroy

13.1.1 The Copy Constructor

A constructor is the copy constructor if its first parameter is a reference to the class type and any additional parameters have default values:

class Foo {
public:
    Foo();            // default constructor
    Foo(const Foo&);  // copy constructor
};

The copy constructor usually should not be explicit.

When we do not define a copy constructor for a class, the compiler synthesises one for us. Unlike the synthesised default constructor, a copy constructor is synthesised even if we define other constructors.

The synthesised copy constructor for some classes prevents us from copying objects of that class type.

The compiler copies each non-static member in turn from the given object into the one being created.

When we use copy initialisation, we are asking the compiler to copy the right-hand operand into the object being created, converting that operand if necessary.

Copy initialisation ordinarily uses the copy constructor. However, if a class has a move constructor, then copy initialisation sometime uses the move constructor instead of the copy constructor.

Copy initialisation happens not only when we define variables using an =, but also when we:

  • Pass an object as an argument to a parameter of non-reference type.

  • Return an object from a function that has a non-reference return type.

  • Brace initialise the elements in an array or the members of an aggregate class.

13.1.2 The Copy-assignment Operator

As with the copy constructor, the compiler synthesises a copy-assignment operator if the class does not define its own.

The copy-assignment operator takes an argument of the same type as the class:

class Foo {
public:
    Foo& operator=(const Foo&); // assignment operator
};

Assignment operators ordinarily should return a reference to their left-hand operand.

13.1.3 The Destructor

The destructor do whatever work is needed to free the resources used by an object and destroy the non-static data members of the object.

class Foo {
public:
    ~Foo(); // destructor
};

Because it takes no parameter, it cannot be overloaded.

The implicit destruction of a member of built-in pointer type does not delete the object to which that pointer points. The members that are smart pointers are automatically destroyed during the destruction phase.

The destructor is used automatically whenever an object of its type is destroyed:

  • Variables are destroyed when they go out of scope.

  • Members of an object are destroyed when the object of which they are a part is destroyed.

  • Elements in a container, whether a library container or an array, are destroyed when the container is destroyed.

  • Dynamically allocated objects are destroyed when the delete operator is applied to a pointer to the object.

  • Temporary objects are destroyed at the end of the full expression in which the temporary was created.

The destructor is not run when are reference or a pointer to an object goes out of scope.

The compiler defines a synthesised destructor for any class that does not define its own destructor.

It is important to realise that the destructor body does not directly destroy the members themselves. Members are destroyed as part of the implicit destruction phase that follows the destructor body. A destructor body executes in addition to the member-wise destruction that takes place as part of destroying an object.

13.1.4 The Rule of Three/Five

One rule of thumb to use when you decide whether a class needs to define its own versions of the copy-control members is to decide first whether the class needs a destructor. Often, the need for a destructor is more obvious than the need for the copy constructor or assignment operator. If the class needs a destructor, it almost surely needs a copy constructor and copy-assignment operators as well.

A second rule of thumb: If a class needs a copy constructor, it almost surely needs a copy-assignment operator, and vice versa.

13.1.5 Using = default

We can explicitly ask the compiler to generate the synthesised versions of the copy-control members by defining them as = default.

13.1.6 Preventing Copies

Most classes should define, either implicitly or explicitly, the default and copy constructors and the copy-assignment operator.

Under the new standard, we can prevent copies by defining the copy constructor and copy-assignment operator as deleted functions.

struct NoCopy {
    NoCopy(const NoCopy&) = delete;
    NoCopy &operator=(const NoCopy&) = delete;
};

The = delete signals to the compiler that we are intentionally not defining these members.

Unlike = default, = delete must appear on the first declaration of a deleted function. This difference follows logically from the meaning of these declarations. A defaulted member affects only what code the compiler generates; hence the = default is not needed until the compiler generates code. On the other hand, the compiler needs to know that a function is deleted in order to prohibit operations that attempt to use it.

Also unlike = default, we can specify = delete on any function (we can use = default only on the default constructor or a copy-control member that the compiler can synthesise).

If a member has a deleted destructor, then that member cannot be destroyed. If a member can't be destroyed, the object as a whole can't be destroyed.

For some classes, the compiler defines these synthesised members as deleted functions:

  • The synthesised destructor is defined as deleted if the class has a member whose own destructor is deleted or is inaccessible (e.g., private).

  • The synthesised copy constructor is defined as deleted if the class has member whose own copy constructor is deleted or inaccessible. It is also deleted if the class has a member with a deleted or inaccessible destructor.

  • The synthesised copy-assignment operator is defined as deleted if a member has a deleted or inaccessible copy-assignment operator, or if the class has a const or reference member.

  • The synthesised default constructor is defined as deleted if the class has a member with a deleted or inaccessible destructor; or has a reference member that does not have an in-class initialiser; or has a const member whose type does not explicitly define a default constructor and that member does not have an in-class initialiser.

Prior to the new standard, classes prevented copies by declaring their copy constructor and copy-assignment as private.

Classes that want to prevent copying should define their copy constructor and copy-assignment operators using = delete rather than making those members private.

13.2 Copy Control and Resource Management

13.2.1 Classes That Act Like Value

to implement value-like behaviour class needs:

  • A copy constructor that copies the data, not just the pointer.

  • A destructor to free the data.

  • A copy-assignment operator to free the object's existing data and copy the data from its right-hand operand.

HasPtr& HasPtr::operator=(const HasPtr &rhs) {
    auto newp = new string(*rhs.ps);    // copy the underlying string
    delete ps;    // free the old memory
    ps = newp;    // copy data from rhs into this object
    i = rhs.i;
    return *this;
}

// WRONG way to write an assignment operator
HasPtr& HasPtr::operator=(const HasPtr &rhs) {
    delete ps;    // fress the string to which this object points
    // if rhs and *this are the same object, we're copying from delete memory
    ps = new string(*(rhs.ps));
    i = rhs.i;
    return *this;
}

13.2.2 Defining Classes That Act Like Points

The easiest way to make a class act like a pointer is to use shared_ptr to manage the resources in the class.

Sometime, we want to manage a resource directly. In such cases, it can be useful to use a reference count. Reference counting works as follows:

  • In addition to initialising the object, each constructor creates a counter. This counter will keep track of how many objects share state with the object we are creating. When we create an object, there is only one such object, so we initialise the counter to 1.

  • the copy constructor does not allocate a new counter; instead, it copies the data members of its given object, including the counter. the copy constructor increments this shared counter, indicating that there is another user of that object's state.

  • The destructor decrements the counter, indicating that there is one less user of the shared state. If the count goes to zero, the destructor deletes that state.

  • The copy-assignment operator increments the right-hand operand's counter and decrements the counter of the left-hand operand.

The counter usually store in dynamic memory.

HasPtr::~HasPtr() {
    if (--*use == 0) {
        delete ps;
        delete use;
    }
}

HasPtr& HasPtr::operator=(const HasPtr &rhs) {
    ++*rhs.use; // increment the use count of the right-hand operand
    if (--*use == 0) {
        delete ps;
        delete use;
    }
    ps = rhs.ps;
    i = rhs.i;
    use = rhs.use;
    return *this;
}

13.3 Swap

Unlike the copy-control members, swap is never necessary. However, defining swap can be an important optimisation for classes that allocate resources.

Classes that define swap often use swap to define their assignment operator. These operators use a technique known as copy and swap.

HasPtr& HasPtr::operator=(HasPtr rhs) {
    swap(*this, rhs);
    return *this;
}

The interesting thing about this technique is that it automatically handles self assignment and is automatically exception safe.

Assignment operators that use copy and swap are automatically exception safe and correctly handle self-assignment.

13.4 Classes That Manage Dynamic Memory

The classes will have three pointers into the space it uses for its elements:

  • elements, which points to the first element in the allocated memory.

  • first_free, which points just after the last actual element.

  • cap, which points just past the end of the allocated memory.

and it will also have four utility functions:

  • alloc_n_copy will allocate space and copy a given range of elements.

  • free will destroy the constructed elements and deallocate the space.

  • chk_n_alloc will ensure that there is room to add at least one more element to the class.

  • reallocate will reallocate the class when it runs out of space.

The class body defines several of tis members:

  • The default constructor default initialises alloc and initialises the pointers to nullptr, indicating that there are no elements.

  • The size member returns the number of elements actually in use, which is equal to first_free - elements.

  • The capacity member returns the number of elements that the class can hold, which is equal to cap - elements.

  • The chk_n_alloc causes the class to be reallocated when there is no room to add another element, which happens when cap == first_free .

  • The begin and end members return pointers to the first and one past the last constructed element, respectively.

The push_back function:

void StrVec::push_back(const string& s) {
    chk_n_alloc();
    alloc.construct(first_free++, s);
}

The alloc_n_copy member is called when we copy or assign a StrVec:

pair<string*, string*>
StrVec::alloc_n_copy(const string *b, const string *e) {
    auto data = alloc.allocate(e - b);
    return {data, uninitalised_copy(b, e, data)};
}

The free member has two responsibilities: It must destroy the elements and then deallocate the space that this StrVec itself allocated:

void StrVec::free() {
    if (elements) {
        for (auto p = first_free; p != elements; )
            alloc.destroy(--p);
        alloc.deallocate(elements, cap - elements);
    }
}

The copy-control members:

StrVec::StrVec(const StrVec &s) {
    auto newdata = alloc_n_copy(s.begin(), s.end());
    elements = newdata.first;
    first_free = cap = new_data.second;
}

StrVec::~StrVec() { free(); }

StrVec& StrVec::operator=(const StrVec &rhs) {
    auto data = alloc_n_copy(rhs.begin(), rhs.end());
    free();
    elements = data.first;
    first_free = cap = data.second;
    return *this;
}

The reallocate member will:

  • allocate memory for a new, larger array of data.

  • construct the first part of that space to hold the existing elements.

  • destroy the elements in the existing memory and deallocate that memory.

void StrVec::reallocate() {
    auto newcapacity = size() ? 2 * size() : 1;
    auto newdata = alloc.allocate(newcapacity);
    auto dest = newdata;
    auto elem = elements;
    for (size_t i = 0; i != size(); ++)
        alloc.construct(dest++, std::move(*elem++));
    free();
    element = newdata;
    first_free = dest;
    cap = elements + newcapacity;
}

13.5 Moving Objects

In some of these circumstances, an object is immediately destroyed after it is copied. A second reason to move rather than copy occurs in classes such as the IO or unique_ptr classes. These classes have a resource that may not be shared.

13.5.1 Rvalue References

To support move operations, the new standard introduced a new kind of reference, an rvalue reference. An rvalue reference is a reference that must be bound to an rvalue. An rvalue reference is obtained by using && rather than &. As a result, we are free to "move" resources from an rvalue reference to another object.

An lvalue expression refers to an object's identity whereas an rvalue expression refers to an object's value.

int i = 42;
int &r = i;    // ok: r refers to i
int &&rr = i;  // error: cannot bind an rvalue reference to an lvalue
int &r2 = i * 42;    // error: i * 42 is an rvalue
int &&rr2 = i * 42;  // ok: bind rr2 to the result of the multiplication

We can also obtain an rvalue reference bound to an lvalue by calling a new library function named move, which is defined in the utility header.

int &&rr3 = std::move(rr1);

13.5.2 Move Constructor and Move Assignment

Move constructors and move assignment operators that cannot throw exceptions should be marked as noexcept.

class StrVec {
public:
    StrVec(StrVec&&) noexcept;
};
StrVec::StrVec(StrVec &&s) noexcept {}

The move-assignment operator:

StrVec& StrVec::operator=(StrVec &&rhs) noexcpet {
    if (this != &rhs) {
        free();
        element = rhs.elements;
        first_free = rhs.first_free;
        cap = rhs.cap;
        // leave rhs in a destructible state
        rhs.element = rhs.first_free = rhs.cap = nullptr;
    }
    return *this;
}

After a move operation, the "moved-from" object must remain a valid, destructible object but users may make no assumptions about its value.

The compiler synthesises the move constructor and move assignment only if a class does not define any of its own copy-control members and only if all the data members can be moved constructed and move assigned, respectively.

If we explicitly ask the compiler to generate a move operation by using =default, and the compiler is unable to move all the members, then the move operation will be defined as deleted. With one important exception, the rules for when a synthesised move operation is defined as deleted are analogous to those for the copy operations:

  • Unlike the copy constructor, the move constructor is defined as deleted if the class has a member that defines tis own copy constructor but does not also define a move constructor, or if the class has a member that doesn't define its own coy operations and for which the compiler is unable to synthesise a move constructor.

  • The move constructor or move-assignment operator is defined as deleted if the class has a member whose own move constructor or move-assignment operator is deleted or inaccessible.

  • Like the copy constructor, the move constructor is defined as deleted if the destructor is deleted or inaccessible.

  • Like the copy-assignment operator, the move-assignment operator is defined as deleted if the class has a const or reference member.

When a class has both a move constructor and a copy constructor, the compiler uses ordinary function matching to determine which constructor to use.

StrVec v1, v2;
v1 = v2;
StrVec getVec(istream&);
ve = getVec(cin);

If a class has a usable copy constructor and no move constructor, objects will be "moved" by the copy constructor. Similarly for the copy-assignment operator and move-assignment.

If we add a move constructor to this class, it will effectively get a move assignment operator as well:

class HasPtr {
public:
    HasPtr(HasPtr &&p) noexcept: ps(p.ps), i(p.i) { p.ps = 0; }
    HasPtr& operator=(HasPtr rhs) { swap(*this, rhs); return *this; }
};

The new library defines a move iterator adaptor. A move iterator adapts its given iterator by changing the behaviour of the iterator's dereference operator. The dereference operator of a move iterator yields an rvalue reference.

make_move_iterator(begin());

13.5.3 Rvalue References and Member Function

void push_back(const X&); // copy:binds to any kind of X
void push_back(X&&);      // move:binds only to modifiable rvalues of type X

Overloaded functions that distinguish between moving and copying a parameter typically have one version that takes a const T& and one that takes a T&&.

StrVec vec;
string s = "some string or another";
vec.push_back(s);    // calls push_back(const string&);
vec.push_back("done");    // calls push_back(sting&&);

Just as we can overloaded a member function based on whether it is const, we can also overload a function based on its reference qualifier.

class Foo {
public:
    Foo sorted() &&;
    Foo sorted() const; // error must have reference qualifier;
    Foo sorted() const&; // ok
    
    Foo sorted(Comp*);
    Foo sorted(Comp*) const;

If a member function has a reference qualifier, all the versions of that member with the same parameter list must have reference qualifiers.

Last updated

Was this helpful?