CPP/C++0x/TheBigFive

From ProgrammingExamples
< CPP
Jump to: navigation, search

In C++0x, whenever writing a resource-holding class, where it is required to perform deep copies, it usually also makes sense to provide move-semantics via a move constructor and a swap function. This can be called the Rule of Five (or The Big Five), that is, you should provide a copy-constructor, a move-constructor, a swap function, a copy-assignment (via copy-and-swap or move-and-swap), and a destructor to free the resources. Below is an example of a simple vector class which holds a dynamic array, and thus, is required to provide the Big Five.

The copy-constructor is implemented as in C++03/98, performing a deep-copy by allocating a new dynamic array and copying the data from the source object. Note that this constructor can also benefit from delegating constructors in C++0x, although not yet supported on most compilers (21/07/2011).

The move-constructor is responsible for two things: "stealing" the resource from the source object and nullifying the resources that the source object holds. This is important. Move-semantics enable two things, it allows one to move the resources from one object to another and it allows perfect forwarding of temporary variables. Since the object to which the rvalue-reference refers to will eventually get destructed, it is important that its resources be nullified such that its destructor has no effect (and does not free the resources that are now held by the new object). As shown below, a move-constructor typically looks like a shallow copy of the data members, followed by setting the source object to a "zombie-state".

Then, another very important function to implement for resource-holding classes is the swap() function. This function swaps the content of two object. This is implemented as it is usual in C++03/98. As shown below, parameters are taken as non-const references (not rvalue-ref) because it only makes sense to swap lvalued objects. Second, the first line of any swap() function is usually the "using std::swap;" statement which allows the ADL (Argument Dependent Lookup) to consider the standard swap function, from the <algorithm> header. All data members of the class should have either a custom swap() function in their own namespace or be satisfied with being swapped by the standard function, and thus, ADL will select the appropriate overloaded swap function (provided that the std::swap name is imported). This also justifies the presence of a swap function, as a friend function, because whenever there is a better way to swap to objects than using the standard function (which performs three deep-copies), then it is justified to have a swap friend function.

Now, with a swap function, the assignment operator can be implemented via the standard copy-and-swap idiom. However, now, the assignment operator can serve a double purpose, as a copy-assignment and as a move-assignment. The standard copy-and-swap idiom passes the object by value, as opposed to the usual const-reference, because this will use the copy-constructor to actually create the copy (i.e. the deep-copy implementation only needs to exist in the copy-constructor), and then swaps the "this" object with the copy. This process leaves the temporary parameter with the original resources of the "this" object and the "this" object with a deep-copy of the source object. This implementation is simple, efficient, minimizes code duplication, and provides strong exception safety (as only the copy-constructor can fail, not the swap, if an exception occurs, the assignment will never be performed and the "this" object will safely keep its original state). When moving towards C++0x and move-semantics, no extra effort is required to implement a "move-and-swap" because, by passing by value to the assignment, the move-constructor will be selected to construct the parameter if that is the most sensible option given the calling context (either explicitly using std::move() or if assigned to a temporary rvalue).

Finally, a destructor is needed which will release the resources correctly, unless RAII is used for all data members, of course.

TheBigFive.cpp

#include <iostream>
#include <algorithm>
 
class my_vector {
  private:
    int* ptr;
    unsigned int sz;
  public:
 
    int& operator[](unsigned int i) { return ptr[i]; };
    const int& operator[](unsigned int i) const { return ptr[i]; };
    unsigned int size() const { return sz; };
 
    my_vector(unsigned int aSize = 0) : 
              ptr((aSize ? new int[aSize] : nullptr)), 
              sz(aSize) { };
 
    // 1 - Copy-Constructor
    my_vector(const my_vector& rhs) : 
              ptr((rhs.sz ? new int[rhs.sz] : nullptr)), 
              sz(rhs.sz) { 
      std::copy(rhs.ptr, rhs.ptr + rhs.sz, ptr); 
    };
 
/*  1 - Copy-Constructor using delegating constructors (not yet supported by GCC)
    my_vector(const my_vector& rhs) : my_vector(rhs.sz) {
      std::copy(rhs.ptr, rhs.ptr + rhs.sz, ptr);
    };
*/
 
    // 2 - Move-Constructor
    my_vector(my_vector&& rhs) : 
              ptr(rhs.ptr), 
              sz(rhs.sz) { 
      rhs.ptr = nullptr; 
      rhs.sz = 0; 
    };
 
    // 3 - Swap function
    friend void swap(my_vector& lhs, my_vector& rhs) throw() {
      using std::swap;
      swap(lhs.ptr,rhs.ptr);
      swap(lhs.sz,rhs.sz);
    };
 
    // 4 - Copy-assignment operator
    my_vector& operator=(my_vector rhs) {
      swap(*this,rhs);
      return *this;
    };
 
    // 5 - Destructor
    ~my_vector() {
      delete[] ptr;
    };
 
};
 
int main() {
  my_vector v(3);
  v[0] = 0; v[1] = 1; v[2] = 2;
 
  my_vector v1(v); //copy-constructor.
  std::cout << "v.size  = " << v.size() << std::endl;
  std::cout << "v1.size = " << v1.size() << std::endl;
 
  my_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 = my_vector(4); //move-assignment, since RHS is an rvalue.
  std::cout << "v2.size = " << v2.size() << std::endl;
 
  return 0;
};

CMakeLists.txt

cmake_minimum_required(VERSION 2.6)
 
PROJECT(TheBigFive)
 
ADD_EXECUTABLE(TheBigFive TheBigFive.cpp )
 
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -std=c++0x")