Copy semantics and resource management in C++

Table of contents

Introduction

Resource management is something that a C++ programmer must do all the time. Resources can include memory blocks, OS kernel objects, multi-threaded locks, network connections, database connections and just any object, created in dynamic memory. Access to a resource is carried out through a handle, a type of a handle is usually a pointer or one of its aliases (HANDLE, etc.), sometimes integer (UNIX file handles). After using a resource, it is necessary to release it, otherwise, sooner or later, an application that does not release resources (and possibly other applications) will face a shortage of resources. This problem is very acute; it can be said that one of the key features of the .NET, Java and several other platforms is a unified resource management system based on garbage collection.

Object-oriented features of C++ naturally lead to the following solution: the resource managing class contains a resource handle as a member, initializes the handle when the resource is acquired, and releases it in the destructor. However, the thing isn’t that simple as it seems to be. The main problem is coping semantics. If the class, that manages a resource, uses the copy constructor generated by the compiler by default, then, after copying the object, we get two copies of the handle of the same resource. If one object releases the resource, the second will possibly try to use or to release the released resource, which in any case is incorrect and can lead to so-called undefined behavior, for example, an application crash.

Fortunately, in C++, we can fully control the copying process by custom defining of a copy constructor and a copy assignment operator, which allows to solve the above problem, and usually not in one way. The implementation of copying should be closely linked to the resource release mechanism, and generally will be called copy-ownership policy. The well-known “Rule of The Big Three” states that if a programmer has defined at least one of the three operations — a copy constructor, a copy assignment operator, or a destructor — he must define all three operations. Copy-ownership policies specify the way to do it. There are four basic copy-ownership policies.

1. Basic copy-ownership policies

Before a resource is acquired or after it has been released, the handle must take on a special value indicating that it is not associated with the resource. It is usually zero, sometimes -1, casted to the type of handle. In any case, such a handle will be called null handle. The class that manages the resource must recognize the null handle and not try to use or release the resource in this case.

1.1. No copying policy

This policy is the simplest. In this case, it is simply forbidden to copy and assign class instances. The destructor releases the acquired resource. In C++, it is not difficult to prohibit copying; the class must declare, but not define, a private copy constructor and a private copy assignment operator.

class X
{
private:
    X(const X&);
    X& operator=(const X&);
// ...
};

Attempts to copy and assign are stopped by the compiler and the linker.

The C++11 standard offers a special syntax for this case:

class X
{
public:
    X(const X&) = delete;
    X& operator=(const X&) = delete;
// ...
};

This syntax is more demonstrative and gives more understandable compiler messages when attempting to copy and assign.

In the previous version of the standard library (C++98), the no copying policy used I/O stream classes (std::fstream, etc.), in Windows many classes from MFC (CFile, CMutex, CEvent, etc.). In the C++11 standard library, some classes for multi-threaded synchronization use this policy.

1.2. Exclusive ownership policy

In this case, during the implementing of copying and assignment, the resource handle moves from the source object to the target object, that means it remains in a single copy. After copying or assignment, the source object has a null handle and cannot use the resource. The destructor releases the acquired resource. In C++11, this is done in the following way: regular copying and copy assignment are prohibited in the manner described above, and move semantics are implemented, that is, the move constructor and the move assignment operator are defined. (More about move semantics on.)

class X
{
public:
    X(const X&) = delete;
    X& operator=(const X&) = delete;
    X(X&& src) noexcept;
    X& operator=(X&& src) noexcept;
// ...
};

Thus, the exclusive ownership policy can be considered an extension of the no copying policy.

In the C++11 standard library, this policy uses a smart pointer std::unique_ptr<> and some other classes, for example: std::thread, std::unique_lock<>, as well as classes that previously used the no copying policy (std::fstream, etc.). In Windows, MFC classes that previously used the no copying policy also has started to use the exclusive ownership policy. (CFile, CMutex, CEvent, etc.).

1.3. Deep copying policy

In this case, a programmer can copy and assign class instances. A copy constructor and a copy assignment operator must be defined, so that the target object copies the resource to itself from the source object. After that, each object owns its own copy of the resource, and can independently use, modify, and release the resource. The destructor releases the acquired resource. Sometimes for objects using the deep copying policy, the term “value objects” is used.

This policy can’t be applied to all resources. It can be used for resources associated with a memory buffer, such as strings, but it is not very clear how to apply it to OS kernel objects such as files, mutexes, etc.

The deep copy policy is used in all types of object strings, std::vector<>, and other containers in the standard library.

1.4. Shared ownership policy

In this case, one can copy and assign class instances. It is necessary to define the copy constructor and the copy assignment operator, in which a resource handle (as well as other data) is copied, but not the resource itself. After that each object has its own copy of the handle, can use, modify, but not release the resource, while there is at least one more object that has a copy of the handle. The resource is released after the last object that has a copy of the handle goes out of scope. The way it can be implemented is described below.

Smart pointers often use this policy; it is also natural to use it for immutable resources. In the C++11 standard library this policy is implemented by the smart pointer std::shared_ptr<>.

2. Deep copying policy – problems and solutions

Let’s consider a function template for exchange of states of objects of type T in the C++98 standard library.

template<typename T>
void swap(T& a, T& b)
{
    T tmp(a);
    a = b;
    b = tmp;
}

If the T type owns a resource and uses the deep copying policy, we have three operations of allocating a new resource, three operations of copying and three operations of releasing resources. Whereas in most cases this operation can be carried out without allocating new resources and copying, it is enough for objects to exchange internal data, including the resource handle. There are a lot of similar examples when it’s necessary to create temporary copies of a resource and release them right there. Such an inefficient implementation of daily operations has stimulated the search for solutions for optimization. Consider the main variants.

2.1. Copy-on-write

Copy-on-write (COW) can be taken as an attempt to combine a deep copy policy and a shared ownership policy. Initially, when the object is copied, the resource handle is copied without the resource itself, and for the owners the resource becomes shared and available in read-only mode. But as soon as some owner needs to modify the shared resource, the resource is copied and the owner works with its own copy. The implementation of the COW solves the problem of exchanging states: there is no additional resource allocation and copying. Using COW is quite popular when implementing strings; for example, CString (MFC, ATL). A discussion about possible ways to implement COW and problems that may arise can be found in [Meyers1], [Sutter]. [Guntheroth] proposes an implementation of COW using std::shared_ptr<>. There are problems when implementing COW in a multi-threaded environment, which is why in the C++11 standard library it is forbidden to use COW for strings, see [Josuttis], [Guntheroth].

The evolution of the COW idea leads to the following resource management scheme: the resource is immutable and is controlled by objects using the shared ownership policy. If necessary, a new, appropriately modified, resource is created and a new owner object is returned. This scheme is used for strings and other immutable objects on .NET and Java platforms. In functional programming it is used for more complex data structures.

2.2. Definition of the state exchange function for a class

Above it was shown how inefficiently the state exchange function, implemented straightforward, through copying and assignment, can work. But the function is widely-spread, for example, it is used by many algorithms of the standard library. In order the algorithms use not std::swap(), but another function, specifically defined for a class, one must perform two steps:

  1. To define in the class the member function Swap() (the name is not essential) that implements the exchange of states.
class X
{
public:
    void Swap(X& other) noexcept;
// ...
};

It’s necessary to make sure, that this function does not throw exceptions; in C++11 such functions must be declared as noexcept.

  1. In the same namespace as the X class (usually in the same header file) to define the free (non-member) function swap() as follows (name and signature fundamental):
inline void swap(X& a, X& b) noexcept { a.Swap(b); }

After that standard library algorithms will use it, not std::swap(). This provides a mechanism called argument dependent lookup (ADL). For more on ADL, see [Dewhurst1].

In the C++ standard library all containers, smart pointers, and other classes implement the state exchange function in the manner described above.

The Swap() member function is usually easy to define: it is necessary to apply a state exchange operation to the bases and members if they support it, and std::swap() otherwise.

The above description is somewhat simplified; more detailed information can be found in [Meyers2]. Some discussion of issues related to the state exchange function can also be found in [Sutter/Alexandrescu].

The state exchange function can be attributed to one of the basic operations of the class. Using it a programmer can gracefully define other operations. For example, a copy assignment operator is defined by copying and Swap() as follows:

X& X::operator=(const X& src)
{
    X tmp(src);
    Swap(tmp);
    return *this;
}

This pattern is called the “copy-and-swap” idiom, see [Sutter], [Sutter/Alexandrescu], [Meyers2] for more details. Its modification can be applied to the implementation of move semantics, see sections 2.4, 2.6.1.

2.3. Removing of intermediate copies by the compiler

Consider the class

class X
{
public:
    X(/* parameters */);
// ...
};

and the function

X Foo()
{
// ...
    return X(/* arguments */);
}

For a straight-line approach, returning from Foo() is accomplished by copying an instance of X. But some compilers can remove a copy operation from code; an object is created directly at the call point. This is called the return value optimization (RVO). RVO has been used by compiler developers for a long time and it is currently fixed in the C++ 11 standard. Although it’s a compiler which decides on RVO, a programmer can write code for its use. For this, it is desirable that the function has one return point and the type of the returned expression matches the type of the return value of the function. In some cases, it is advisable to define a special private constructor, called a “computational constructor”, for more details, see [Dewhurst2]. RVO is also discussed in [Meyers3] and [Guntheroth].

Compilers can remove intermediate copies in other situations as well.

2.4. Implementing move semantics

The implementation of move semantics consists of defining the move constructor that has rvalue-reference parameter to the source and the move assignment operator with the same parameter.

In the C++11 standard library the state exchange function template is defined as follows:

template<typename T>
void swap(T& a, T& b)
{
    T tmp(std::move(a));
    a = std::move(b);
    b = std::move(tmp);
}

In accordance with the overload resolution rules for functions that have parameters of rvalue-reference type (see Appendix A), in the case the T type has a move constructor and a move assignment operator, exactly they will be used, and temporary copies will not be created. Otherwise,the copy constructor and the copy assignment operator will be used.

The use of move semantics let a programmer to avoid the creation of temporary copies in a much wider context than the state exchange function described above. Move semantics is applied to any rvalue value, that is a temporary, unnamed value, as well as to a return value of the function if it is created locally (including the lvalue), and no RVO has been applied. In all these cases, it is guaranteed that the source object cannot be used in any way after performing the move. Move semantics also is used with a lvalue value to which the std::move() conversion is applied. But in this case, the programmer himself is responsible for how the source objects will be used after the move (for example std::swap()).

The C++11 standard library has been reworked to reflect move semantics. The move constructor and the move assignment operator, as well as other member functions, with rvalue-reference parameters, have been added to many classes. For example, std::vector<T> has an overloaded version of void push_back(T&& src). All-in-all it allows to avoid creating temporary copies in many cases.

Implementing move semantics does not override the definition of the state exchange function for the class. Specially defined state exchange function can be more efficient than the standard std::swap(). Moreover, the move constructor and the move assignment operator are very easily defined using the member function of the state exchange as follows (a variation of the “copy-and-swap” idiom):

class X
{
public:
    X() noexcept {/* null handle initialization */}
    void Swap(X& other) noexcept {/* state exchange */}

    X(X&& src) noexcept : X()
    {
        Swap(src);
    }

    X& operator=(X&& src) noexcept
    {
        X tmp(std::move(src)); // moving
        Swap(tmp);
        return *this;
    }
// ...
};

For the move constructor and the move assignment it is highly desirable to make sure that they do not throw exceptions, and, accordingly, they are declared as noexcept. This allows a user to optimize some operations of the containers of the standard library without violating the strong exception guarantee (for more details, see [Meyers3] and [Guntheroth] ). The proposed pattern gives such a guarantee, provided that the default constructor and the state exchange member function do not throw exceptions.

The C++11 standard considers that a compiler automatically generates a move constructor and a move assignment operator; to do this, they must be declared using the "= default" construct.

class X
{
public:
    X(X&&) = default;
    X& operator=(X&&) = default;
// ...
};

The operations are implemented by sequentially applying the move operation to bases and class members if they support moving, and copy operation otherwise. Obviously, this variant is far from always acceptable. Raw handles do not support moving, but they usually cannot be copied. If certain conditions are met, the compiler may independently generate such a move constructor and a move assignment operator, but it is better not to use this opportunity, these conditions are rather confusing and can be easily changed when the class is refined. See [Meyers3] for details.

In general, implementing and using move semantics is not so elementary. The compiler can apply copying where the programmer expects movement. Let us cite a few rules that allow to eliminate or at least reduce the probability of such a situation.

  1. To prohibit copying if possible.
  2. To declare a move constructor and a move assignment operator as noexcept.
  3. To implement move semantics for base classes and members.
  4. To apply the std::move() conversion to the function parameters of rvalue-reference type.

Rule 2 was discussed above. Rule 4 is related to the fact that named rvalue-references are lvalues (see also Appendix A). This can be illustrated by defining a move constructor.

class B
{
// ...
    B(B&& src) noexcept;
};

class D : public B
{
// ...
    D(D&& src) noexcept;
};

D::D(D&& src) noexcept
    : B(std::move(src)) // moving
{/* ... */}

Another example of this rule is given above, when defining a move assignment operator. Implementing move semantics is also discussed in section 6.2.1.

2.5. Emplacement vs insertion

The idea of emplacement is similar to the idea of underlying RVO (see section 2.3), but it is applied not to the return value of the function, but to the input parameters. In the traditional insertion of an object into a container, an object is first created (often temporary), then copied or moved to the storage location, after which the temporary object is deleted. When emplacing, the object is created immediately in the storage location, only the arguments of the constructor are passed. Containers of the C++11 standard library have the emplace(), emplace_front(), emplace_back() member functions that work this way. Naturally, they are template member functions with a variable number of template parameters — variadic templates, since the number and the type of constructor parameters are not known in advance. In addition to this other advanced C++11 features are used – perfect forwarding and universal references.

Emplacement has the following advantages:

  1. For objects, that do not support moving, the copy operation is excluded.
  2. For objects, that support moving, placement is almost always more efficient.

Here is an example where the same problem is solved in different ways.

std::vector<std::string> vs;
vs.push_back(std::string(3, 'X')); // insertion
vs.emplace_back(3, '7');           // emplacement

In the case of insertion, a temporary std::string is created, then it is moved to the storage location and after that the temporary object is deleted. When emplacing, the object is created immediately in the storage location. Emplacement looks more succinct and it is likely to be more efficient. Scott Meyers discusses emplacement, perfect forwarding and universal references in details in [Meyers3].

2.6. Summary

One of the main problems of the classes, that implement a deep copying policy, is the creation of temporary copies of a resource. None of the described methods completely solves this problem and does not completely replace any other method. In any case, a programmer must recognize such situations and write the correct code, taking into account the problem described and the possibilities of the language. The simplest example is the passing parameters to the function: it is necessary to pass by reference, and not by value. This error is not recognized by the compiler, but either unnecessary copying occurs or a program does not work as intended. Another example is related to using a move: a programmer must strictly observe the conditions under which the compiler chooses a move, otherwise copying will be used “silently”.

The described problems allow us to make the following recommendation: it is necessary to avoid the deep copying policies whenever possible; the real need for deep copying occurs very rarely, this is confirmed by the programming experience on the .NET and Java platforms. As an alternative, an implementation of deep copying using a special function can be suggested, the traditional name for such functions is Clone() or Duplicate().

If, nevertheless, implementing the class that manages a resource, it is decided to use the deep copying policy, the following steps can be recommended in addition to implementing copying semantics:

  1. To define the state exchange function.
  2. To define a move constructor and a move assignment operator.
  3. To define necessary member functions and free functions with parameters of rvalue-references type.

On the .NET and Java platforms, the basic copy-ownership policy is the shared ownership policy, but if necessary, one can implement the deep copy policy, for example, in .NET it is necessary to implement the IClonable interface. As noted above, the need for this occurs quite rarely.

3. Possible variants for implementing shared ownership policy

It is quite easy to implement the shared ownership policy for a resource, that has an internal reference counter. In this case, when copying an object – a resource owner – the reference counter is incremented, and in the destructor it is decremented. When its value reaches zero, the resource releases itself. The internal reference counter uses basic Windows OS resources: OS kernel objects, managed through HANDLE, and COM objects. For kernel objects, the reference counter is incremented by the DuplicateHandle() function, and decremented by the CloseHandle() function. For COM objects, the member functions IUnknown::AddRef() and IUnknown::Release() are used. The smart pointer CComPtr<> from ATL library controls COM objects that way. For UNIX file handles, opened using C standard library functions, the reference counter is incremented by the _dup() function, decremented using the file close function.

In the standard C++ library the smart pointer std::shared_ptr<> also uses a reference count. But the object, controlled by this smart pointer, may not have an internal reference counter, so a special hidden object is created. It is called a control block and it manages the reference counter. It is some additional overhead. The smart pointer std::shared_ptr<> is described in detail in [Josuttis], [Meyers3].

The use of the reference counter has a congenital defect: if resource owner objects have reciprocal references to each other, their reference counters will never be zero (the problem of circular references). In some cases, resources cannot have reciprocal references (for example, OS kernel objects) and therefore this problem is not relevant, but in other cases, the programmer himself must monitor such situations and take the necessary efforts. When using std::shared_ptr<>, it is suggested to use the auxiliary smart pointer std::weak_ptr<> for this purpose. See more in [Josuttis], [Meyers3].

Andrei Alexandrescu considers the implementing a joint ownership policy using a doubly linked list of owner objects [Alexandrescu]. Herbert Schildt describes (and lists the complete code) implementation, based on the combination of a doubly linked list and the [Schildt] reference counter. Implementations, based on a doubly linked list, also cannot release resources that have circular references.

Description of more complicated schemes for removing unused objects (garbage collectors) can be found in [Alger].

The implementation of the shared ownership policy should also include the possibility of multi-threaded access to the owner objects. This topic is discussed in [Josuttis] and [Alexandrescu].

The shared ownership policy is the main copy-ownership policy on the .NET and Java platforms. The component of the executing environment, which deals with the removal of unused objects, is called garbage collector, runs periodically and uses complex algorithms for analyzing an object graph.

4. Exclusive ownership policy and move semantics

Safe implementation of the exclusive ownership policy became possible only after C++ began to support the rvalue-references and move semantics. In the C++98 standard library there was a smart pointer std::auto_ptr<> that implemented the exclusive ownership policy. However, it had limited use, in particular, it could not be stored in containers. The fact is that it could move the pointer from the object which still needed this pointer (simply speaking, to steal). In C++11, rules for using rvalue references ensure that data can only be moved from a temporary unnamed object, otherwise occurs a compilation error. In the C++11 standard library std::auto_ptr<> is deprecated and it is recommended to use std::unique_ptr<> instead. This smart pointer implements the exclusive ownership policy based on move semantics, it is described in [Josuttis], [Meyers3].

Some other classes also support the exclusive ownership policy: I/O stream classes (std::fstream, etc.), some classes for working with threads (std::thread, std::unique_lock<>, etc.). In MFC, the classes that previously used the no copying policy (CFile, CEvent, CMutex, etc.) has started to apply the exclusive ownership policy.

5. No copying policy – quick start

At first glance the no copying policy severely limits the programmer, but it turns out that many objects do not need to be copied. Therefore, when designing the class that manages a resource, as an initial solution, we can recommend to choose the no copying policy. If copying is required, the compiler will immediately detect it; after that a programmer can analyze what copying is needed for (and whether it’s required at all) and make the necessary improvements. In some cases, for example, when passing through a call stack, the reference can be applied. If it is necessary to store objects in the containers of the standard library, a programmer can use smart pointers to objects, created in dynamic memory. In general, the use of dynamic memory and smart pointers is a universal variant that can help in other cases. A more complicated way is the implementation of move semantics. Details are discussed in section 6.

Negligent software design, which does not implement any copy-ownership policy, often does not lead to runtime errors, because the objects that own the resource are not actually copied. In this case, the no copying or another copy-ownership policy doesn’t change anything. But, nevertheless, it still has to be done, one must always write the proper code, even if in some context the wrong code does not show its defects. The wrong code will “shoot” sooner or later.

6. The life cycle of the resource and the object-owner of the resource

In many cases it is important to understand how the life cycle of a resource and its object-owner correlate. Naturally, this is closely related to the copy-ownership policy. Let’s have a look at several variants.

6.1. Resource acquisition is initialization

In the simplest case the life cycle of a resource and the object-owner are the same, which means, for the class that manages the resource, the following conditions are met:

  1. Resource acquisition occurs only in a class constructor. If the acquisition fails, an exception is thrown, and the object is not created.
  2. Resource release occurs only in the destructor.
  3. Copying and moving are prohibited.

Constructors of such classes usually have the parameters, necessary to acquire the resource, and, accordingly, there is no default constructor. In the standard C++11 library some classes are implemented to support multi-threaded synchronization.

This resource management scheme is one of the variants of the “Resource acquisition is initialization” (RAII) idiom. RAII is widely observed in many books and on the Internet (and is often interpreted slightly differently or simply not quite clearly), see, for example, [Dewhurst1]. The above variant can be called “strict” RAII. In such a class it is natural to make the resource handle a constant member, and, accordingly, it’s possible to use the term immutable RAII.

6.2. Extended variants for managing the life cycle of a resource

A class, implemented in accordance with the RAII idiom, is ideal for creating simple, short-lived objects that have a block lifetime. However, if an object must be either a member of another class, or an element of an array or some container, the absence of a default constructor, as well as copy-move semantics, can cause many problems for a programmer. In addition, sometimes a resource is acquired in several steps, and their number may not be known in advance, which makes it extremely difficult to implement a resource acquisition in the constructor. Consider possible solutions to this problem.

6.2.1. Extended life cycle of a resource

Let’s agree, that the class, managing a resource, supports the extended life cycle of the resource if the following conditions are met for it:

  1. There is a default constructor which doesn’t acquire the resource.
  2. There is a mechanism for resource acquisition after creating an object.
  3. There is a resource release mechanism before the object is destroyed.
  4. The destructor releases the acquired resource.

In the standard C++11 library the extended life cycle of a resource is supported by strings, containers, smart pointers, and also some other classes. But keep in mind that the clear() member function, implemented in strings and containers, destroys all stored objects, but may not release the reserved memory. For the complete release of all resources it is necessary to take additional measures. For example, you can use shrink_to_fit(), or simply assign an object, created by the default constructor (see below).

The class, implemented in accordance with the RAII idiom, can be refined, using a standard pattern so that it will support the extended life cycle of the resource. To do this one must additionally define a default constructor, a move constructor, and a move assignment operator.

class X
{
public:
// RAII
    X(const X&) = delete;            // copy ban
    X& operator=(const X&) = delete; // assignment ban

    X(/* parameters */);             // acquires a resource
    ~X();                            // releases the resource
// add
    X() noexcept;                    // zeros the resource handle
    X(X&& src) noexcept              // move constractor
    X& operator=(X&& src) noexcept;  // move assignment operator
// ...
};

After this, the extended life cycle of the resource is implemented quite simply.

X x;                    // creating an "empty" object
x = X(/* arguments */); // resource acquisition
x = X(/* arguments */); // new resource acquisition, old releasing
x = X();                // resource releasing

This is exactly how the std::thread is implemented.

As it was shown in section 2.4, a standard way of defining a move constructor and a move assignment operator uses the member state exchange function. Besides, the member state exchange function makes it very plain to define individual member functions to acquire and release the resource. Here is the corresponding new variant:

class X
{
// RAII
// ...
public: // add an variant with a member state exchange function
    X() noexcept;
    X(X&& src) noexcept;
    X& operator=(X&& src) noexcept;
    void Swap(X& other) noexcept;  // exchanges states
    void Create(/* parameters */); // acquires a resource
    void Close() noexcept;         // releases the resource
// ...
};

X::X() noexcept {/* the resource handle zeroing */}

Definition of a move constructor and a move assignment operator:

X::X(X&& src) noexcept : X()
{
    Swap(src);
}

X& X::operator=(X&& src) noexcept
{
    X tmp(std::move(src)); // moving
    Swap(tmp);
    return *this;
}

Definition of the individual member functions to acquire and release the resource:

void X::Create(/* parameters */)
{
    X tmp(/* arguments */); // resource acquisition
    Swap(tmp);
}

void X::Close() noexcept
{
    X tmp;
    Swap(tmp);
}

It should be noted that in the pattern described the resource is always acquired in the constructor, and released in the destructor, the member state exchange function plays a purely technical role. This simplifies the coding and makes its more reliable for the acquisition and release of a resource, since the compiler takes over part of the implementation logic, especially in the destructor.

In the examples above the definitions of the copy assignment operator and the resource acquisition member function used the copy-and-swap idiom, according to which a new resource is acquired first, then the old one is released. This scheme provides so-called strong exception guarantee: if an exception occurs during the acquisition of a resource, the object won’t change its state (transactional semantics). In certain situations another scheme may be more preferable: the old resource is released first, then a new one is acquired. This variant provides a weaker exception guarantee, called the basic one: if an exception occurs during the acquisition of a resource, the object will not necessarily remain in the same state, but the new state will be correct. In addition, when defining a copy assignment operator using this scheme, a self-assignment check is desirable. Details of exception safety guarantees are discussed in [Sutter], [Sutter/Alexandrescu], [Meyers2].

So, the transition from RAII to the extended life cycle of the resource is very similar to the transition from the no copying policy to the exclusive ownership policy.

6.2.2. Single resource acquisition

This variant can be considered as intermediate between RAII and the extended life cycle of the resource. We will say that the class, that manages the resource, uses a single resource acquisition if the following conditions are met for it:

  1. There is a default constructor which doesn’t acquire the resource.
  2. There is a mechanism for resource acquisition after creating an object.
  3. Repeated resource acquisition is prohibited. If such an attempt occurs, an exception is thrown.
  4. Resource release occurs only in the destructor.
  5. Copying is prohibited.

This is “almost” RAII, the only difference is the possibility for a formal separation of an object creating operation and resource acquisition. Such a class may have a move constructor, but not a move assignment operator, otherwise the condition of clause 3 will be violated. This simplifies the objects storage in standard containers. Despite some unfinished business, this variant is quite practical.

6.2.3. Increase indirectness

Another approach to extending the life cycle of a resource is to increase the level of indirectness. In this case, the RAII object itself is considered as a resource, and the pointer to it will be a resource handle. Resource acquisition is reduced to creating an object in dynamic memory, and, releasing it – to deleting the object. As a class that manages such a resource, one can use one of the standard library’s smart pointers. A copy-ownership policy is determined by a smart pointer. This method is much simpler than that described in section 6.2.1, the only drawback is the more intensive use of dynamic memory.

7. Summary

The class, that manages a resource, should not have a copy constructor, a copy assignment operator, and a destructor generated by the compiler. These member functions must be defined according to a copy-ownership policy.

There are 4 basic copy-ownership policies:

  1. No copying policy.
  2. Exclusive ownership policy.
  3. Deep copying policy.
  4. Shared ownership policy.

The state exchange function should be attributed to the basic operations of the class. It is used in the standard library algorithms, as well as for defining other member functions of the class: a copy assignment operator, a move constructor and a move assignment operator, member functions to acquire and release the resource.

The definition of a move constructor and a move assignment operator allows to optimize classes that use a deep copying policy. For the classes that use the no copying policy, this allows to expand the copy-ownership policy, implement a more flexible resource lifecycle management scheme and simplify the placement of objects in containers.

When designing the class that owns a resource, we can recommend the following sequence of actions. It is appropriate to begin with no copying policy. If compilation reveals the need for copying and cannot be avoided by simple means, it’s requied to create objects in dynamic memory and use smart pointers to control their lifetime (see section 6.2.3). If this variant does not suit, the move semantics have to be implemented (see section 6.2.1). One of main consumers of copying is the containers of the standard library; the implementation of move semantics removes almost all restrictions on their use.

As mentioned above, implementation of the deep copying policy is better to be avoided, the real need for it rarely arises. Implementation of the shared ownership policy is also a bad idea; instead, use the smart pointer std::shared_ptr<>.

Appendices

Appendix A. Rvalue-references

Rvalue-references are a type of regular C++ references, the difference is in the initialization rules and the overloaded resolution rules for functions, that have parameters of the rvalue-references type. The rvalue-references type for type T is denoted by T&&.

For examples the class is used:

class Int
{
    int m_Value;

public:
    Int(int val) : m_Value(val) {}
    int Get() const { return m_Value; }
    void Set(int val) { m_Value = val; }
};

Like regular references, rvalue-references must be initialized.

Int&& r0; // error C2530: 'r0' : references must be initialized

The first difference between rvalue-references and regular C++ references is that they cannot be initialized using lvalue. Example:

Int i(7);
Int&& r1 = i; // error C2440: 'initializing' : cannot convert from 'Int' to 'Int&&'

For correct initialization, you need to use rvalue:

Int&& r2 = Int(42); // OK
Int&& r3 = 5;       // OK

or lvalue must be explicitly casted to an rvalue-reference type:

Int&& r4 = static_cast<Int&&>(i); // OK

Instead of the cast operator, the function std::move() is usually used (more precisely, a function template), which does the same (the header file <utility>).

Rvalue references can be initialized with a rvalue of a built-in type, which is forbidden for regular references.

int&& r5 = 2 * 2; // OK
int& r6 = 2 * 2;  // error

After initialization, the rvalue-references can be used as regular references.

Int&& r = 7;
std::cout << r.Get() << '\n'; // Output: 7
r.Set(19);
std::cout << r.Get() << '\n'; // Output: 19

Rvalue-references are implicitly casted to regular references.

Int&& r = 5;
Int& x = r;           // OK
const Int& cx = r;    // OK

Rvalue-references are rarely used as independent variables, they are usually applied as function parameters. In accordance with the initialization rules, if a function has parameters of rvalue-references type, it can be called only for rvalue arguments.

void Foo(Int&&);

Int i(7);
Foo(i);            // error, lvalue argument
Foo(std::move(i)); // OK
Foo(Int(4));       // OK
Foo(5);            // OK

If there are several overloaded functions, overload resolution rules claim that for the rvalue arguments the version with a parameter of type rvalue-reference takes benefits over a version with a parameter of type regular reference or regular reference to a constant, although the latter may be a valid variant. And this rule is the second key feature of rvalue-reference.

The function with a parameter, passed by value, and an overloaded version, that has a parameter of type rvalue-reference, will be ambiguous for the rvalue arguments.

For example, consider overloaded functions

void Foo(Int&&);
void Foo(const Int&);

and several variants to call them

Int i(7);
Foo(i);            // Foo(const Int&)
Foo(std::move(i)); // Foo(Int&&)
Foo(Int(6));       // Foo(Int&&)
Foo(9);            // Foo(Int&&)

Attention is drawn to one important point: a named rvalue-reference itself is an lvalue.

Int&& r = 7;
Foo(r);            // Foo(const Int&)
Foo(std::move(r)); // Foo(Int&&)

This should be taken into account when defining functions, having the parameters of type rvalue-reference. Such parameters are lvalue and may require the use of std::move(). See an example of a move constructor and a move assignment operator in section 2.4.

Another C++11 innovation, related to rvalue-references, is the reference qualifiers for non-static member functions. They allow to overload by type (lvalue/rvalue) of the hidden parameter this.

class X
{
public:
    X();
    void DoIt() &;  // this points to lvalue
    void DoIt() &&; // this points to rvalue
// ...
}; 

X x;
x.DoIt();   // DoIt() &
X().DoIt(); // DoIt() &&

Appendix B. Move semantics

For classes, owning a resource like a memory buffer and useing the deep copy policy (std::string, std::vector<>, etc.), the problem of preventing the creation of temporary copies of the resource is essential . One of the most effective ways to solve this problem is to implement move semantics. For this, the move constructor, that has a parameter of type rvalue-reference to the source, and the move assignment operator with the same parameter are defined. When they are implemented, the data, including the resource handle, is copied from the source object to the target object, and the resource handle of the source object is reset; the resource is not copied. In accordance with the above overload resolution rules, in the case when the class has a copy constructor and a move constructor, the move constructor will be used for initialization with rvalue, and the copy constructor for initialization with lvalue. If the class has only a move constructor, the object can be initialized only with rvalue. The assignment operators works the same. Move semantics is also used when returning a value, created locally, from a function (including an lvalue), unless RVO has been applied.

Bibliography

[Alexandrescu]
Alexandrescu, Andrei. Modern C++ Design. Addison-Wesley, 2001.

[Alger]
Alger, Jeff. C++ for Real Programmers. Academic Press, 1998.

[Dewhurst1]
Dewhurst, Stephen C. C++ Common Knowledge. Addison-Wesley, 2005.

[Dewhurst2]
Dewhurst, Stephen C. C++ Gotchas: avoiding common problems in coding and design. Addison-Wesley, 2002.

[Guntheroth]
Guntheroth, Kurt. Optimized C++. O’Reilly, 2016.

[Josuttis]
Josuttis, Nicolai M. The C++ Standard Library – A Tutorial and Reference, 2nd Edition. Addison-Wesley, 2012.

[Meyers1]
Meyers, Scott. More effective C++: 35 New Ways to Improve Your Programs and Designs. Addison-Wesley, 1996.

[Meyers2]
Meyers, Scott. Effective C++: 55 Specific Ways to Improve Your Programs and Designs, 3rd Edition. Addison-Wesley, 2005.

[Meyers3]
Meyers, Scott. Effective Modern C++: 42 Specific Ways to Improve Your Use of C++11 and C++14. O’Reilly, 2015.

[Schildt]
Schildt, Herbert. The Art of C++. McGraw-Hill, 2004.

[Sutter]
Sutter, Herb. Exceptional C++. Addison-Wesley, 2005.

[Sutter/Alexandrescu]
Sutter, Herb and Alexandrescu, Andrei. C++ Coding Standards. Addison-Wesley, 2005.