Beginner's Guide to C++0x:
The Big Five - Implementing a Resource-holding RAII Class
This tutorial will show how to implement a resource-holding class showing practical use of C++0x features. A resource-holding class is any class which holds, with its data members, a resource which could include:
- dynamically allocated memory
- an internet connection socket
- a file stream
- any other handle to an operating system resource (e.g. window or thread handle), or
- an opaque pointer from an external library.
The main characteristic of a resource-holding class is the requirement to allocate (or create) the resource (capturing a handle or pointer to it) and then releasing the resource.
This type of class is fundamental to practical programming because a C++ programmer will very often face situations in which low-level data structures are needed or when it is required to interface C++ code to external libraries (or APIs) which often provide C-compatible interfaces based on handles or opaque pointers ("opaque pointer" is a general term for a pointer which is provided via some API functions and should only be acted on (or dereferenced) from within the API functions, not by the user of the API, so, to the user, it is not really known to what it points to, and is thus "opaque"). The preferred strategy to handle these situations is to create a C++ class which wraps all those low-level functions and API calls, and provide automatic, encapsulated handling of those low-level details, hiding them from the user, leaving him with only an abstract knowledge of the behaviour and features provided by the class.
Generally, the RAII idiom is the founding paradigm to design this type of class. RAII stands for Resource Acquisition Is Initialization which means that the resources should be acquired as the class is being initialized, and conversely, be freed when the class is finalized (destructed). This idiom allows the class to be used with value-semantics and with automatic behaviour. Value-semantics mean that an object of the class can be created and treated as a value-type (e.g. built-in types like int or double, or familiar RAII classes of the C++ standard libraries like std::string or std::vector). The automatic behaviour means that the object automatically takes care of itself, in other words, the user of the class is not required to run special finalizing code when the object is no longer needed, he can simply let the object go out-of-scope and be destroyed, triggering automatic cleaning or releasing of any resources held by the object. This idiom not only makes code cleaner and simpler by not requiring finalizing code, but it also makes it easier to create exception-safe code (see advanced remarks).
Another nice side-effect of RAII classes is that the composition of RAII classes is also a RAII class, that is, if a class contains only RAII data members (primitive-types included) and inherits only from RAII classes, then that class will be RAII as well, with no additional programming effort. This is why it is recommend to wrap things in RAII classes at the lowest possible level, because it will help making all the higher-level classes RAII as well, without additional effort.
Put simply, if you find it generally easier to use std::vector
in your everyday programming, as opposed to C-style dynamically allocated arrays, then you are already aware of at least some of the benefits of RAII. This should be enough to convince you that RAII is preferred. This tutorial will show the basic design pattern that will enable you to make your own RAII classes, such that your classes are just as nice to use as std::vector
or other C++ standard classes (which are all RAII classes, for good reason).
There are two main types of resource-holding classes: copyable and non-copyable classes. We first tackle the copyable case, then, the non-copyable case follows naturally. It will be assumed that the resource in question cannot be allocated using a RAII class, which is often the case for OS handles and low-level programming. To illustrate the concept, we will use the simple example of a custom-made "int_vector" class which holds a dynamically allocated array of integers. Of course, in real code, it is recommended to use RAII classes if they are available, in this case, std::vector<int>
would be the class of choice if we were not constructing a tutorial example, but writing real code.
Caveat: Although this is a beginner's guide, we do have to assume some basic knowledge in C++. The reader should know how to write a simple class, understand pointers and references, be aware of general OOP syntax such as creating data-members and member functions, writing constructors with initialization lists, destructors, operator overloading, and creating objects.
*********************** Advanced Remarks **************************
RAII and Exception-safety
As mentioned, RAII has benefits for exception-safety, let us expand on this point for advanced readers. In C++, the preferred channel for signaling exceptional situations is by throwing exceptions. This assertion will not be defended here because it has been discussed at great lengths by most pilars of C++: Stroustrup, Sutter & Alexandrescu, Abrahams, and Cline. Most of these articles make good arguments for the use of RAII and Exceptions as fundamental companions in a comprehensive error-handling strategy. Here, "exceptional situations" refers to, for example, running out of memory, failing to open an internet connection, failing to open a file or read the expected data from it, or even detecting numerical ill-conditioning, like a singular matrix or a diverging numerical iteration. In other words, exceptions are used to branch out of the normal course of action, i.e. the good execution path, and switch to some recovery or backup solution (for example, a matrix inversion method detects a singular matrix, throws an exception, and the handler switches to a pseudo-inversion method that can cope with a singular matrix).
The use of exceptions do cause certain complications (or special considerations) because of a powerful feature of exception-handling: stack unwinding. As code gets executed in C++, objects get created on the stack as a function progresses, when an exception is thrown, all fully-constructed objects on the stack get destroyed in the reverse order that they were constructed (and if an object's constructor failed, all the fully-constructed data members get destructed, but the object's destructor is not called because it could not be fully constructed). The stack unwinds until the start of a try-block is reached, and then exception handlers are looked at for a match. The idea behind stack unwinding is to revert the state of the program back to where it was when the try-block started (i.e. succeed-or-rollback). Obviously, the destructors are key elements in this process, their job is to make sure that everything that was done upon construction is undone (or released). It's pretty obvious that RAII plays a central role in accomplishing this correctly, and this topic is referred to as "exception-safety". In C++, we talk about three levels of exception-safety for some operation performed in or on an object:
- Weak exception-safety: If an exception is thrown during the operations, the object's state will still be or restored to a valid state (not necessarily in the original state), no resources are leaked, and it is guaranteed that the stack unwinding will not crash (no-throw destructors).
- Strong exception-safety: The program state, upon recovery from an exception, will be exactly the same as it was before the failed-operation was attempted. In other words, the operation either succeeds, or fails and leaves the state unaffected.
- No-throw guarantee: The operation can never fail, no exception will ever be thrown by the operation.
This tutorial will remark upon exception-safety levels and guidelines, within "Advanced Remarks" sections like this one, because RAII and the Big Five are so closely tied to exception-safety.
*******************************************************************
A Copyable Resource-holding Class
A copyable resource-holding class usually requires what is referred to as a deep-copy mechanism. If we want to truly copy a resource-holding class, simply copying the handle or pointer to the resource is not sufficient, that is, a shallow-copy is not enough. It requires allocation of a new resource and duplication (or copying) of the data from the source to the destination. This requires a custom copy-constructor, copy-assignment operator, and some additional functions, overall there are five of them, they are called The Big Five.
We start by laying out the basic functionality of our int_vector
class:
class int_vector {
private:
int* array_ptr; //holds a pointer that refers to a dynamically allocated array (the resource)
unsigned int array_sz; //holds the size of the array that array_ptr points to.
public:
// We provide basic vector operators (const and non-const versions):
int& operator[](unsigned int i) { return array_ptr[i]; };
const int& operator[](unsigned int i) const { return array_ptr[i]; };
unsigned int size() const { return array_sz; };
};
Of course, our int_vector
class is not yet a useful class, because it has no useful member functions to actually allocate the array. How should we allocate this array and initialize the size of the int_vector
object? Let's examine our options.
First, we could expose the data members by making them public, and then initialize the object as so:
int_vector v;
v.array_ptr = new int[5];
v.array_sz = 5;
Anyone would agree, I hope, that this is a pretty horrible solution, but why? It's bad because it puts all the responsibility on the user to maintain a correct object-state and avoiding leaks. The users cannot be trusted with such a responsibility, and it also makes the class somewhat useless since it doesn't really add much functionality to a simple C-style array.
Second, we could initialize the data members directly via a constructor as so:
class int_vector {
//.. as before
public:
int_vector(int* aPtr, unsigned int aSize) : array_ptr(aPtr), array_sz(aSize) { };
};
//..
int_vector v(new int[5], 5);
Surely, this is a bit better than the first option, at least in terms of verbosity. But it is still a horrible solution. The first problem with the solution is that the validity of the object's state is not enforced or enforcable. The validity of an object's state is usually referred to as the class invariants, i.e, a set of relationships that are always held true by the class's implementation details. In our example, the class invariant is the fact that the pointer array_ptr
always points to an array that contains array_sz
elements (or NULL
if array_sz
is zero). There is no way to assert this condition upon construction with the above code, i.e., nothing stops the user from creating int_vector v(new int[5], 1)
, and such a situation cannot be detected. This is a fundamental rule in class design in C++: "know your class-invariants and guarrantee that they hold at all times, from construction to destruction". The number one responsibility of a class implementation is to protect, assert and enforce its invariants. Some classes don't have invariants, i.e., all the data members are independent, and it's often acceptable to expose those data members as public in this case. But resource-holding classes always have invariants, and it is never acceptable to expose their data members as public.
Another major problem with our two options for constructing our int_vector
objects is ownership. Who owns the resource that array_ptr
points to? Who is responsible for destroying it? How was it created? With new? With malloc? On the stack? These are all ambiguous questions with no obvious answers, and this can have disastrous consequences. There is no doubt that one of the major sources of memory leaks, memory corruption, and heap corruption in non-trivial software is ambiguity about ownership. Invest in clear specifications about ownership (e.g. with smart-pointers) and you will be well rewarded for it. A resource-holding class is, by definition, a class that is unambiguous about resource ownership. A resource-holding object owns the resource, doesn't share it and will put its life on the line to protect it, literally (this will be elaborated later).
The third option reduces the class invariants to its essential element, that is, the size of the array, and possibly its content. For sake of simplicity, we will just make a constructor to initialize the array to a given size:
class int_vector {
//.. as before
public:
int_vector(unsigned int aSize = 0) :
array_ptr( ( aSize ? new int[aSize] : nullptr ) ),
array_sz(aSize) { };
};
//..
int_vector v(5);
Now, our int_vector
class controls the allocation of the memory, and can thus control its deallocation. The class invariants are guaranteed to hold upon construction. We also get a default constructor (a constructor with no parameter) for free by giving a default value to the aSize
parameter. The above constructor also introduces one new feature of C++0x, that is, the nullptr
. This built-in keyword replaces the NULL
pointer from old-style C++ which was generally implemented as a #define
for 0
or ((void*)0)
, which can give rise to some problems, nullptr
is preferred.
In C++0x, the introduction of std::initializer_list
allows the implicit construction of a generic, constant static array using a {1,2,3,4}
type of syntax (the same as initializing C-style static arrays). The std::initializer_list
is essentially similar to a standard STL container like std::vector
. So, we can define an additional constructor like so:
class int_vector {
//.. as before
public:
int_vector(std::initializer_list<int> aV) :
array_ptr( ( aV.size() ? new int[aV.size()] : nullptr ) ),
array_sz(aV.size()) {
std::copy(aV.begin(), aV.end(), array_ptr); //copy the content from the initializer_list to the array_ptr.
};
};
//..
int_vector v{1,2,3,4,5}; //construction syntax with {} enclosed lists.
int_vector v2 = {6,7,8,9,10};
*********************** Advanced Remarks **************************
Some advanced readers might remark upon the dynamic allocation of memory within the initialization list of the constructor. There is a popular belief (or myth) that one should not write code that could throw an exception in the initialization list. This is a distortion of a more complex issue. There is nothing wrong or dangerous about an exception being thrown during the execution of an initialization list. It is just as "dangerous" to initialize a std::vector data member to a given size than to allocate a C-style dynamic array of a given size, they both will throw std::bad_alloc
if dynamic allocation is impossible for some reason (out-of-memory, heap-corruption, etc.).
This myth comes from a more complex issue related to catching exceptions in an initialization list. Some believe that exceptions cannot be caught, within the constructor, if they are thrown from the initialization list, this is also false, the "function try-block" syntax allows those exceptions to be caught. The problem, however, is that an exception thrown in the initialization list cannot be kept from propagating out of the constructor (there are good reasons for that, but it's too complex a matter). So, the rule should be that if you want to avoid exceptions from leaking out of a constructor, you should have only no-throw constructor calls in the initialization list and execute the potentially throwing code within a try-block within the body of the constructor.
In our int_vector
example, we don't have a "backup" solution for the case where the allocation throws a std::bad_alloc
exception. We could set the array_ptr
to nullptr
and array_sz
to zero, but this would require the user to check, after construction, that the int_vector
was correctly constructed. This would essentially be an error-code mechanism that requires the user to "do, check, do, check, etc." which is one of the things exceptions are designed to avoid. In this case, it's preferrable to let the exception leak, and thus, notifying the user about the problem along the preferred channel of exception-handling.
*******************************************************************
The Fifth - Destructor
Starting with the last, it's now time to address the fifth of the Big Five, that is, the destructor. In our int_vector class, since the array is strictly owned by the object, a destructor is required to free the resource. This is quite straight forward since we will ensure that class-invariants are preserved. Because the invariants will be protected in our class (once completed), we are sure that the array_ptr
is either nullptr
or is pointing to memory that was dynamically allocated using the new[]
operator. Since deleting a nullptr
has no effect, it is safe to simply write:
class int_vector {
//.. as before
public:
~int_vector() {
delete[] array_ptr;
};
};
That's it. We are done with the destructor.
*********************** Advanced Remarks **************************
Some readers might be tempted to ask why we wouldn't specify the no-throw guarantee for the destructor, via a throw()
statement. This is not needed for a destructor because a destructor is implicitly non-throwing, and if it is throwing, it is a big problem, destructors should never throw an exception, and the compiler assumes that.
*******************************************************************
The First - Copy-constructor
As mentionned before, a copyable resource-holding class will generally require a deep-copy of its data. This implies the definition of a custom copy-constructor. If one does not implement a copy-constructor, the compiler will generate one (and similarly for all of the Big Five). The default behaviour of a compiler-generated copy-constructor is to perform a shallow-copy because it will simply copy the values of all the data members into the destination object. When holding resources via pointers or handles, simply copying the values will not copy the resources they refer to, they will simply be referring to the same resource. A shallow-copy is a big problem because it breaks the ownership assumption: the resource-holding object should be the unique owner of the resource. If two objects end up referring to the same resource, who owns it? This cannot be allowed, our resource-holding class will not share its resource, but will allow it to be duplicated (or "cloned", or deeply-copied).
*********************** Advanced Remarks **************************
Another alternative to deep-copy is the so-called "copy-on-write" optimization (or lazy-copying). The idea behind this optimization is to avoid performing the deep-copy for as long as the data is not changed by either the source object or the destination object. This optimization is a bit tricky to implement, especially in a multi-threaded environment, and exception-safety is also harder to acheive. Also, it's sometimes undesirable due to the inherent lack of control, by the user, over the moment at which the deep-copy is performed, which can make performance somewhat unpredictable. This optimization will not be discussed here, but could be an interesting subject for a future, more advanced tutorial.
*******************************************************************
A deep-copy is fairly simple to implement, at least in our example case. The idea of a deep-copy is to recreate a new object with the same "capacity" as the source object, and copy all the data (or clone the resource). The end result should be that the two objects are equal in every meaningful way, but are (completely) independent in terms of memory and resource ties, i.e., if one object gets destroyed it won't affect the other. For our int_vector
class, this can be realized by creating an array of the same size as the source object, and copying all the elements from it:
class int_vector {
//.. as before
public:
int_vector(const int_vector& src) : //take the source object by const-reference.
array_ptr( ( src.array_sz ? new int[src.array_sz] : nullptr ) ),
array_sz(src.array_sz) {
std::copy(src.array_ptr, src.array_ptr + src.array_sz, array_ptr); //copy the content from the source to the array_ptr.
};
};
In C++0x, one can delegate constructor calls, meaning that one constructor can call another directly in the initialization list. This is useful to minimize code-duplication, which minimizes typos and errors. Unfortunately, at this time (July 2011), few compilers support this feature, but if it is supported, we could implement the copy-constructor as so:
class int_vector {
//.. as before
public:
// Constructor as before:
int_vector(unsigned int aSize = 0) :
array_ptr( ( aSize ? new int[aSize] : nullptr ) ),
array_sz(aSize) { };
// Copy-constructor which delegates to the above constructor:
int_vector(const int_vector& src) :
int_vector( src.array_sz ) { //delegate to the other constructor
std::copy(src.array_ptr, src.array_ptr + src.array_sz, array_ptr); //copy the content from the source to the array_ptr.
};
// Initializer-list constructor with delegation as well:
int_vector(std::initializer_list<int> aV) :
int_vector( aV.size() ) { //delegate to the other constructor
std::copy(aV.begin(), aV.end(), array_ptr);
};
};
As one can see, the above allows the resource acquisition to appear only once in the class's implementation. This can be very useful because for more complex types of resources, it's not desirable to duplicate the resource acquisition code since it can be complex and error-prone.
The Second - Move-Constructor
It was previously mentioned that "a resource-holding object should put its life on the line to protect its resource", however, this does not mean that a resource-holding object cannot hand over its resource to another object, it means that if it does so, it must die! This concept is called move-semantics, meaning that the resource is moved from one owner to another. The first step to implementing this is to create a move-constructor.
Under old-style C++ (pre-C++0x), one might be tempted to provide the following constructor prototype to implement move-semantics:
int_vector(int_vector& aV);
Obviously, this type of constructor would allow the transfer of the resource, since the source object can be modified and put into an empty-state (or "zombie-state"). But there are two problems with the above, which make it an impractical solution. First, a temporary object, formally referred to as an rvalue, cannot be bound to a non-const reference, meaning that this constructor will not be able to move resources from a temporary object to the newly constructed object. Second, any object that is not "const" will bind to a non-const reference, if available, instead of a const-reference (from the copy-constructor). This means that if one intends to copy the object (even though it is not "const"), this "move" constructor will be selected, emptying the object without the approval or intend of the user.
C++0x introduced a new feature to solve this problem. This feature is called an rvalue-reference and is denoted using double ampersand symbols, as so:
int_vector(int_vector&& aV) throw();
Rvalue-references can bind to temporary objects (rvalues) and allow modification of them. Because rvalue-references bind to temporary objects, it is safe to assume that it's allowed to "empty" that object, because a temporary object will be destroyed immediately after the constructor/function call. Additionally, if one wishes to bind a non-temporary object (formally called an lvalue) to an rvalue-reference, C++0x provides a function template, called std::move()
, to explicitly do so. In other words, if the user wishes to move the resource held by an object into the new object, he can use this std::move
function to make it explicit that the source object can be "killed" (or emptied, or put in a "zombie-state"). This solves both problems with our previous, naive attempt at implementing a move-constructor. Now, to implement the move-constructor, all that is needed is to make a shallow-copy of the source object, and immediately after, putting the source object in an empty-state. We thus get:
class int_vector {
//.. as before
public:
//Move-constructor:
int_vector(int_vector&& aV) throw() :
array_ptr(aV.array_ptr), //"shallow-copy"
array_sz(aV.array_sz) {
aV.array_ptr = nullptr; //putting the source object in an empty-state (or zombie-state).
aV.array_sz = 0;
};
};
Now, when the move-constructor is invoked, the new object will take over the resource of the original object, while the original object will be left with no content. In other words, the resource is stolen from the source object and the source object is sort-of "killed" (although it is still alive, and thus, the term "zombie-state"). Ownership is transferred from the source object to the new object.
One should realize that, in general, moving a resource-holding object is far more economical than copying it. It is clear that move-semantics are preferred if one does not want to preserve the original object. This is a nice example of an underlying philosophy in C++, that is, you pay for what you get. If you want a copy, you pay the price of a deep-copy, if you don't want a copy, you don't pay the price for it. This was a problem in old-style C++ which required awkward work-arounds to implement move-semantics.
*********************** Advanced Remarks **************************
Acute readers might notice that once you enter a function which takes an rvalue-reference as a parameter, that parameter now is a local variable of the function, and thus, it is now an lvalue, and so are its data members. This causes a bit of a problem when forwarding move-semantics to data members. Assume we stored the array of values as an std::vector<int>
instead of a C-style array, then we would have to use the std::move
function again to move the data member from the source object to the new object, as so:
class int_vector {
private:
std::vector<int> v;
public:
int_vector(int_vector&& aV) :
v( std::move(aV.v) ) { }; //use std::move to take an rvalue-reference to aV.v.
};
Also notice that all STL containers (and pretty much any other C++ standard classes) implement this type of move-constructor. And if a user-defined class does not provide a move-constructor, the compiler will generate one that is much like the above (forwards move-semantics to data members using std::move
).
Generally, move-constructors can be implemented to be non-throwing which is the strongest exception-safety level. If it cannot be made non-throwing, it can usually be made to satisfy strong exception-safety which means that all possibly-throwing operations are done before the resources are "stolen" from the source object (thus, leaving it unchanged if an exception occurs).
*******************************************************************
The Third - Swap Function
We now move on to another fundamental mechanism in programming, that is, swapping two values. Since RAII classes implement value-semantics, it makes sense that their values can be swapped. Like the move-constructor, implementing a swap function can be very advantageous for a resource-holding class, and it was the next-best alternative to move-constructors in pre-C++0x code. If we looked at the standard swap function from the C++03 standard library <algorithm>, we would see something like this:
//C++03 std::swap function synopsis:
template <typename T>
void swap(T& lhs, T& rhs) {
T tmp(lhs);
lhs = rhs;
rhs = tmp;
};
The above works fine for built-in types (like int, double, etc.) or classes which don't hold resources. But for a resource-holding class, it will be very expensive, in general, to perform one copy-construction and two copy-assignments, and it is really not necessary to do so because the two objects could simply exchange the resources they hold. Now, with C++0x, the swap function is somewhat optimized by move-semantics, and would look like this:
//C++0x std::swap function synopsis:
template <typename T>
void swap(T& lhs, T& rhs) {
T tmp(std::move(lhs)); //move lhs --> tmp
lhs = std::move(rhs); //move rhs --> lhs
rhs = std::move(tmp); //move tmp --> rhs
};
In order to benefit from this implementation of std::swap
, we need to implement a move-assignment operator, with this prototype:
int_vector& operator=(int_vector&& rhs);
This could certainly be done, but we will not choose this option because it would require code that is very similar to the move-constructor, and code duplication is not desirable for robustness. We want to keep the "trickier" code (like resource acquisition, release or transfer) from appearing more than once. For practical reasons, we would also like to provide a pre-C++0x way to implement an efficient swap function, for compiling on older implementations. For these reasons, we will overload the swap function for our specific type, and since we will need access to private members, we will make it a friend function. We thus get:
class int_vector {
//.. as before
public:
//Swap function:
friend void swap(int_vector& lhs, int_vector& rhs) throw() {
using std::swap; //include std::swap in ADL.
swap(lhs.array_ptr, rhs.array_ptr); //swap pointers.
swap(lhs.array_sz, rhs.array_sz); //swap sizes.
};
};
And that's it. We can notice that the swap is very efficient, in the same way the move-constructor is. Also note that this swap function can be used in C++03 as a reasonable solution to move an object efficiently (swap a new, empty object with the source object).
*********************** Advanced Remarks **************************
There are several things to notice in the above. First, our swap function is non-throwing because it simply exchanges the values of the pointers and size values. In general, it is possible to implement a no-throw swap for any class, just like it is for the move-constructor. STL containers generally guarantee that swap functions are non-throwing, given some non-throwing properties of some template arguments (mainly for functor copying like copying a Compare
object for std::set
or std::map
). And since the swap function is usually implemented in terms of the swapping the data members, the no-throw guarantee propagates throughout.
The second thing to notice is the using std::swap;
statement. This is not merely to shorten the invocation of swap functions afterwards. The purpose of this statement, which becomes more useful in class templates where the types of some data members are unknown, is to include the std::swap
function into the set of functions which are looked at during the Argument-Dependent Lookup (ADL) of the appropriate overload of the swap function. If a data member is of a custom class with a custom swap function, both in the same namespace, that overload version will naturally be selected since ADL first inspects the namespace of the argument-types for an appropriate overload. If we were to call std::swap
(fully qualified), the appropriate custom swap function would not be found. And of course, if a data member is of a built-in or standard type, then the std::swap
has to be found and used. So, this is the only solution that solves all those ADL issues without having to know which swap function to use for which data member, and let the compiler decide.
*******************************************************************
The Fourth - Assignment Operator
Last but certainly not least, the assignment operator is a crucial part of the Big Five which is presented last because it makes clever use of all the other elements. Again, in the spirit of avoiding code duplication, we don't want to simply write an assignment operator which does essentially the same as what the copy-constructor does. Instead, we want to re-use the copy-constructor to do what the copy-constructor does (the deep-copy) and not repeat that code because it's wasteful and error-prone to do so. So, what does an assignment operator do? Two things need to be done: clear the original data of the destination object and replace it with a copy of the source object. So, naively, we could do:
class int_vector {
//.. as before
public:
int_vector& operator=(const int_vector& aV) {
//destroy:
delete array_ptr;
//copy:
array_ptr = ( aV.array_sz ? new int[aV.array_sz] : nullptr );
array_sz = aV.array_sz;
std::copy(aV.array_ptr, aV.array_ptr + aV.array_sz, array_ptr);
return *this;
};
};
There are a number of problems with this implementation. First, what if one writes int_vector v(3); v = v;
? This is the self-assignment problem. We need to verify that the right-hand-side (rhs) and left-hand-side (lhs) are not the same object, because deleting the array_ptr
would result in deleting aV.array_ptr
as well, which would result in a segmentation-fault (or access-violation) due to reading in deleted memory. So, we could do:
class int_vector {
//.. as before
public:
int_vector& operator=(const int_vector& aV) {
if(this != &aV) { //check for self-assignment
//destroy:
delete array_ptr;
//copy:
array_ptr = ( aV.array_sz ? new int[aV.array_sz] : nullptr );
array_sz = aV.array_sz;
std::copy(aV.array_ptr, aV.array_ptr + aV.array_sz, array_ptr);
};
return *this;
};
};
Another problem with this code is that it is not exception-safe at all. If the memory allocation for the new array fails, the object will be left in a crippled state, and its invariant will be broken. To solve this, we need to first do the operation that could fail, without modifying the state of the object, and then destroy. So, we get:
class int_vector {
//.. as before
public:
int_vector& operator=(const int_vector& aV) {
if(this != &aV) {
//copy:
int* tmp_ptr = ( aV.array_sz ? new int[aV.array_sz] : nullptr );
std::copy(aV.array_ptr, aV.array_ptr + aV.array_sz, tmp_ptr);
//destroy:
delete array_ptr;
//reset:
array_ptr = tmp_ptr;
array_sz = aV.array_sz;
};
return *this;
};
};
So, we know that we should first copy, then destroy and reset. Now, if memory allocation fails, the object will be left in its original state, this provides strong exception-safety.
We should now tackle the main problem with this assignment operator, that is, it essentially contains a duplicate of the copy-constructor's code, the destructor's code and a kind of move-operation (from tmp_ptr to array_ptr). One should notice that it is not important in which order the old resource is destroyed and when it is reset with the new, as long as both operations happen after the copy. Since the first operation is a copy, we might as well, with no additional cost, make the copy into a local variable:
class int_vector {
//.. as before
public:
int_vector& operator=(const int_vector& aV) {
if(this != &aV) {
//copy:
int_vector tmp(aV);
//destroy:
delete array_ptr;
//reset:
array_ptr = tmp.array_ptr;
array_sz = tmp.array_sz;
tmp.array_ptr = nullptr;
tmp.array_sz = 0;
};
return *this;
};
};
We can recognize, in the later part of the above code, that the temporary copy gets moved into the "this" object. So we could replace it with a move-assignment, if we had one, but we don't want one, more on that later. Instead, we will use our swap function which we have chosen to implement instead of the move-assignment operator. And since our class is automatic, the destructor takes care of cleaning resources. So, we can simply put the old resource into the temporary object and leave it to be destroyed as the function finishes, destroying all its local variables. So, a simple swap function call will do the trick of destroying and resetting:
class int_vector {
//.. as before
public:
int_vector& operator=(const int_vector& aV) {
if(this != &aV) {
//copy:
int_vector tmp(aV);
//swap:
swap(*this,tmp);
}; // tmp will be destroyed here and clear the old resource that was originally in *this.
return *this;
};
};
Now, as a general rule when writing a function, if you need a local copy of one of the parameters, it is preferrable to pass-by-value for that parameter. Using a pass-by-value allows for certain optimizations, for example, if the passed object is a temporary, the compiler will usually create it in-place (so the copy is avoided). Also, if the copying fails with an exception (or compilation error), it will show-up at the call-site, which usually more convenient to the user. Additionally, for this assignment operator, passing-by-value takes care of the self-assignment problem, and yields this simple function:
class int_vector {
//.. as before
public:
int_vector& operator=(int_vector aV) { //pass-by-value
swap(*this,aV);
return *this;
}; // aV will be destroyed here and clear the old resource that was originally in *this.
};
The above is called the "copy-and-swap" idiom, and is used extensively in C++ coding. To summarize, we see that we have an assignment operator that is simple, error-free, strongly exception-safe, and does not duplicate code. This copy-and-swap method relies on the copy-constructor, a (no-throw) swap function and an automatic destructor.
But wait! What about move-semantics? Don't we need a move-assignment operator? No. The reason we don't need an additional assignment operator for move-semantics is because we pass-by-value in our assignment operator, and a new object can be created, with move-semantics, using the move-constructor. Thus, if one write v1 = std::move(v2);
, the value-parameter required by our assignment operator will be created using the move-constructor, and then our assignment operator will swap it with the "this" object, leaving the old resource to be destroyed. This is called the "move-and-swap" idiom, and the sweet thing is, we get it for free by passing by value in our copy-and-swap assignment operator.
Our "Complete" Class
So, we are now finished with all the essential elements of a resource-holding class. We can reiterate the complete class definition for clarity:
class int_vector {
private:
int* array_ptr; //holds a pointer that refers to the dynamically allocated memory (the resource)
unsigned int array_sz; //holds the size of the array that array_ptr points to.
public:
// We provide basic vector operators (const and non-const versions):
int& operator[](unsigned int i) { return array_ptr[i]; };
const int& operator[](unsigned int i) const { return array_ptr[i]; };
unsigned int size() const { return array_sz; };
// Default Constructor
int_vector(unsigned int aSize = 0) :
array_ptr( ( aSize ? new int[aSize] : nullptr ) ),
array_sz(aSize) { };
// Initializer-list constructor with delegation
int_vector(std::initializer_list<int> aV) :
int_vector( aV.size() ) {
std::copy(aV.begin(), aV.end(), array_ptr);
};
// 1 - Copy-constructor with delegation
int_vector(const int_vector& src) :
int_vector( src.array_sz ) {
std::copy(src.array_ptr, src.array_ptr + src.array_sz, array_ptr);
};
// 2 - Move-Constructor
int_vector(int_vector&& rhs) :
array_ptr(rhs.array_ptr),
array_sz(rhs.array_sz) {
rhs.array_ptr = nullptr;
rhs.array_sz = 0;
};
// 3 - Swap function
friend void swap(int_vector& lhs, int_vector& rhs) throw() {
using std::swap;
swap(lhs.array_ptr,rhs.array_ptr);
swap(lhs.array_sz,rhs.array_sz);
};
// 4 - Assignment operator
int_vector& operator=(int_vector rhs) {
swap(*this,rhs);
return *this;
};
// 5 - Destructor
~int_vector() {
delete[] ptr;
};
};
int main() {
int_vector v{1,2,3};
int_vector v1(v); //copy-constructor.
std::cout << "v.size = " << v.size() << std::endl;
std::cout << "v1.size = " << v1.size() << std::endl;
int_vector v2(std::move(v)); //move-constructor
std::cout << "v.size = " << v.size() << std::endl;
std::cout << "v2.size = " << v2.size() << std::endl;
v1 = v; //copy-assignment. (copy-and-swap)
std::cout << "v1.size = " << v1.size() << std::endl;
v = std::move(v2); //move-assignment. (move-and-swap)
std::cout << "v.size = " << v.size() << std::endl;
std::cout << "v2.size = " << v2.size() << std::endl;
v2 = int_vector(4); //move-assignment, since RHS is an rvalue.
std::cout << "v2.size = " << v2.size() << std::endl;
return 0;
};
Non-copyable Class
Very often, it makes sense to keep a resource-holding class from being copied, either because a deep-copy of the resource is not possible or because doing so does not make sense. Generally, to make a class non-copyable, in C++03, one can simply make the copy-constructor and copy-assignment private, and thus, not accessible to the user of the class, and keeping the compiler from generating a default version of the copy-constructor and copy-assignment. However, C++0x provides a mechanism to explicitely delete a type of constructor (and it is preferred for technical reasons). Making a class non-copyable does not necessarily mean that we want to disable move-semantics. If we want a movable but non-copyable class, we can delete copy-semantics and provide only move-semantics via a move-constructor and a move-assignment operator, we thus get:
class non_copyable_int_vector {
private:
int* array_ptr; //holds a pointer that refers to the dynamically allocated memory (the resource)
unsigned int array_sz; //holds the size of the array that array_ptr points to.
public:
// We provide basic vector operators (const and non-const versions):
int& operator[](unsigned int i) { return array_ptr[i]; };
const int& operator[](unsigned int i) const { return array_ptr[i]; };
unsigned int size() const { return array_sz; };
// Default Constructor
non_copyable_int_vector(unsigned int aSize = 0) :
array_ptr( ( aSize ? new int[aSize] : nullptr ) ),
array_sz(aSize) { };
// Initializer-list constructor with delegation
non_copyable_int_vector(std::initializer_list<int> aV) :
non_copyable_int_vector( aV.size() ) {
std::copy(aV.begin(), aV.end(), array_ptr);
};
non_copyable_int_vector(const non_copyable_int_vector&) = delete; //non-copyable
non_copyable_int_vector& operator=(const non_copyable_int_vector&) = delete; //non-assignable
// 1 - Move-Constructor
non_copyable_int_vector(non_copyable_int_vector&& rhs) :
array_ptr(rhs.array_ptr),
array_sz(rhs.array_sz) {
rhs.array_ptr = nullptr;
rhs.array_sz = 0;
};
// 2 - Swap function
friend void swap(non_copyable_int_vector& lhs, non_copyable_int_vector& rhs) throw() {
using std::swap;
swap(lhs.array_ptr,rhs.array_ptr);
swap(lhs.array_sz,rhs.array_sz);
};
// 3 - Move-Assignment operator (move-and-swap)
non_copyable_int_vector& operator=(non_copyable_int_vector&& rhs) { //must take a rvalue-reference.
non_copyable_int_vector tmp(std::move(rhs)); //move into a temporary
swap(*this,tmp);
return *this;
};
// 4 - Destructor
~non_copyable_int_vector() {
delete[] ptr;
};
};
RAII Resource
If we can old the resource via a RAII object, we can greatly reduce the Big Five to nothing at all. Because a RAII class has value-semantics, the default copy-constructor and copy-assignment operator are correct to use. And, because RAII classes are automatic, the default destructor is also valid. Finally, because a C++0x RAII class should implement move-semantics, the default move-constructor and move-assignment operator are also correct to use. And finally, the standard swap function (which uses move-assignments) is also perfectly fine to use. We can thus use the C++0x explicit defaulting of special member functions to get:
class int_vector_wrapper {
private:
int_vector v; //holds a RAII resource
public:
// We provide basic vector operators (const and non-const versions):
int& operator[](unsigned int i) { return v[i]; };
const int& operator[](unsigned int i) const { return v[i]; };
unsigned int size() const { return v.size(); };
// Default Constructor
int_vector_wrapper(unsigned int aSize = 0) : v(aSize) { };
// Initializer-list constructor
int_vector_wrapper(std::initializer_list<int> aV) : v(aV) { };
int_vector_wrapper(const int_vector_wrapper&) = default;
int_vector_wrapper& operator=(const int_vector_wrapper&) = default;
int_vector_wrapper(int_vector_wrapper&&) = default;
int_vector_wrapper& operator=(int_vector_wrapper&&) = default;
};
Or, simply:
class int_vector_wrapper {
private:
int_vector v; //holds a RAII resource
public:
// We provide basic vector operators (const and non-const versions):
int& operator[](unsigned int i) { return v[i]; };
const int& operator[](unsigned int i) const { return v[i]; };
unsigned int size() const { return v.size(); };
int_vector_wrapper(unsigned int aSize = 0) : v(aSize) { };
int_vector_wrapper(std::initializer_list<int> aV) : v(aV) { };
};
The above illustrates how composition of RAII classes become RAII themselves with no effort. Here the int_vector_wrapper
is a RAII class, because it holds only RAII data members, no additional programming effort is required.
Conclusions
This tutorial has demonstrated the creation of a basic resource-holding RAII class, with its non-copyable variant. The main lesson to take out of this tutorial is the realization that when resources are held in an object, the number one concern is to preserve the integrity and strict ownership of those resources. We have also shown how features of C++0x can greatly benefit the implementation and use of resource-holding classes, via move-semantics which can either enable a non-copyable class to be moved (and thus, stored in STL containers) as well as providing a powerful optimization that avoids deep-copies when the user does not require one. Additionally, we showed how strong exception-safety can be obtained and how code duplication can be minimized, via the copy-and-swap and the constructor delegation.
It should be noted that your compiler may not support all or any of the features of C++0x, it could take time before full compliance of implementations, and, of course, you must use the most up-to-date compiler you can get.