How to Actually Implement the Rule of Five

How to actually implement the rule of five?

You've missed a significant optimization in your copy assignment operator. And subsequently the situation has gotten confused.

  AnObject& operator = ( const AnObject& rh )
{
if (this != &rh)
{
if (n != rh.n)
{
delete [] a;
n = 0;
a = new int [ rh.n ];
n = rh.n;
}
std::copy(rh.a, rh.a+n, a);
}
return *this;
}

Unless you really never think you'll be assigning AnObjects of the same size, this is much better. Never throw away resources if you can recycle them.

Some might complain that the AnObject's copy assignment operator now has only basic exception safety instead of strong exception safety. However consider this:

Your clients can always take a fast
assignment operator and give it strong
exception safety. But they can't take
a slow assignment operator and make it
faster.

template <class T>
T&
strong_assign(T& x, T y)
{
swap(x, y);
return x;
}

Your move constructor is fine, but your move assignment operator has a memory leak. It should be:

  AnObject& operator = ( AnObject&& rh )
{
delete [] a;
n = rh.n;
a = rh.a;
rh.n = 0;
rh.a = nullptr;
return *this;
}

...

Data a = MakeData();
Data c = 5 * a + ( 1 + MakeMoreData() ) / 3;

q2: Using a combination of copy elision / RVO / move semantics the
compiler should be able to this this
with a minimum of copying, no?

You may need to overload your operators to take advantage of resources in rvalues:

Data operator+(Data&& x, const Data& y)
{
// recycle resources in x!
x += y;
return std::move(x);
}

Ultimately resources ought to be created exactly once for each Data you care about. There should be no needless new/delete just for the purpose of moving things around.

Is this a proper implementation of the Rule of Five (or Rule of Four and 1/2)?

Most important of all:

  • This class doesn't need custom copy/move operations nor the destructor, so rule of 0 should be followed.

Other things:

  • I don't like swap(*this, other); in the move ctor. It forces members to be default-constructed and then assigned. A better alternative would be to use a member initializer list, with std::exchange.

    If initializing all members gets tedious, wrap them in a structure. It makes writing the swap easier too.

  • Copy constructor must take the parameter by a const reference.

  • unique_ptrs which can't be copied. So we move it. is a bad rationale. If your members can't be copied, don't define copy operations. In presence of custom move operations, the copy operations will not be generated automatically

  • Move operations (including the by-value assignment) should be noexcept, because standard containers won't use them otherwise in some scenarios.

  • SomeClass() = default; causes members that are normally uninitialized (int m_int;) to sometimes be zeroed, depending on how the class is constructed. (E.g. SomeClass x{}; zeroes it, but SomeClass x; doesn't.)

    Unless you want this behavior, the constructor should be replaced with SomeClass() {}, and m_int should probably be zeroed (in class body).

Rule-of-Three becomes Rule-of-Five with C++11?

I'd say the Rule of Three becomes the Rule of Three, Four and Five:

Each class should explicitly define exactly one
of the following set of special member
functions:

  • None
  • Destructor, copy constructor, copy assignment operator

In addition, each class that explicitly defines a destructor may explicitly define a move constructor and/or a move assignment operator.

Usually, one of the following sets of special member
functions is sensible:

  • None (for many simple classes where the implicitly generated special member functions are correct and fast)
  • Destructor, copy constructor, copy assignment operator (in this case the
    class will not be movable)
  • Destructor, move constructor, move assignment operator (in this case the class will not be copyable, useful for resource-managing classes where the underlying resource is not copyable)
  • Destructor, copy constructor, copy assignment operator, move constructor (because of copy elision, there is no overhead if the copy assignment operator takes its argument by value)
  • Destructor, copy constructor, copy assignment operator, move constructor,
    move assignment operator

Note:

  • That move constructor and move assignment operator won't be generated for a class that explicitly declares any of the other special member functions (like destructor or copy-constructor or move-assignment operator).
  • That copy constructor and copy assignment operator won't be generated for a class that explicitly declares a move constructor or move assignment operator.
  • And that a class with an explicitly declared destructor and implicitly defined copy constructor or implicitly defined copy assignment operator is considered deprecated.

In particular, the following perfectly valid C++03 polymorphic base class:

class C {
virtual ~C() { } // allow subtype polymorphism
};

Should be rewritten as follows:

class C {
C(const C&) = default; // Copy constructor
C(C&&) = default; // Move constructor
C& operator=(const C&) = default; // Copy assignment operator
C& operator=(C&&) = default; // Move assignment operator
virtual ~C() { } // Destructor
};

A bit annoying, but probably better than the alternative (in this case, automatic generation of special member functions for copying only, without move possibility).

In contrast to the Rule of the Big Three, where failing to adhere to the rule can cause serious damage, not explicitly declaring the move constructor and move assignment operator is generally fine but often suboptimal with respect to efficiency. As mentioned above, move constructor and move assignment operators are only generated if there is no explicitly declared copy constructor, copy assignment operator or destructor. This is not symmetric to the traditional C++03 behavior with respect to auto-generation of copy constructor and copy assignment operator, but is much safer. So the possibility to define move constructors and move assignment operators is very useful and creates new possibilities (purely movable classes), but classes that adhere to the C++03 Rule of the Big Three will still be fine.

For resource-managing classes you can define the copy constructor and copy assignment operator as deleted (which counts as definition) if the underlying resource cannot be copied. Often you still want move constructor and move assignment operator. Copy and move assignment operators will often be implemented using swap, as in C++03. Talking about swap; if we already have a move-constructor and move-assignment operator, specializing std::swap will become unimportant, because the generic std::swap uses the move-constructor and move-assignment operator if available (and that should be fast enough).

Classes that are not meant for resource management (i.e., no non-empty destructor) or subtype polymorphism (i.e., no virtual destructor) should declare none of the five special member functions; they will all be auto-generated and behave correct and fast.

Simple linked list with full set Rule of Five

The first thing to address is that your assignment operator is not correct. You're using the copy / swap idiom, but you forgot to do the copy.

NodeList& NodeList::operator=(NodeList src)  
{
std::swap(head, src.head);
return *this;
}

Note the change from a const NodeList& to NodeList src as the argument. This will make the compiler automatically do the copy for us, since the parameter is passed by value.

If you still want to pass by const reference, the following change would need to be made:

NodeList& NodeList::operator=(const NodeList& src) 
{
if ( &src != this )
{
NodeList temp(src); // copy
std::swap(head, temp.head);
}
return *this;
}

Note the additional test for self-assignment. It really isn't necessary, but may speed up the code (but again, no guarantee).

As to whether this is the most efficient way to do this, that is up for debate -- it all depends on the object. But one thing is for sure -- there will be no bugs, dangling pointers, or memory leaks if you use the copy/swap idiom (correctly).


Now onto the move functions:

To implement the missing functions, you should basically remove the contents from the existing object, and steal the contents from the passed-in object:

First, the move contructor:

NodeList::NodeList(Nodelist&& src) : head{src.head} 
{
src.head = nullptr;
}

All we really want to do is steal the pointer from src, and then set the src.head to nullptr. Note that this will make src destructible, since src.head will be nullptr (and your destructor for NodeList handles the nullptr correctly).

Now for the move-assignment:

Nodelist& operator=(NodeList&& src) 
{
if ( this != &src )
std::swap(src.head, head);
return *this;
}

We check for self assignment, since we don't want to steal from ourselves. In reality, we really didn't steal anything, only swapped things out. However unlike the assignment operator, no copy is done -- just a swap of the internals (this is basically what your incorrect assignment operator that was fixed earlier was doing). This allows the src to destroy the old contents when it's time for the src destructor to be invoked.

Note that after the move (either construction or assignment), the passed-in object is basically in a state that may or may not make the object usable or if not usable, stable (because potentially, the internals of the passed-in object have been changed).

The caller can still use such an object, but with all the risks of using an object that may or may not be in a stable state. Thus the safest thing for the caller is to let the object die off (which is why in the move constructor, we set the pointer to nullptr).

How to properly apply rule of 5 (or zero?) to a class containing a vector of custom objects with strings

If a class deals with the ownership of a resource, then that class should ONLY manage that resource. It shouldn't do anything else. In this case define all 5 (rule of 5).

Else, a class doesn't need to implement any of the 5 (rule of 0).

Simple as that.


Now, I've seen these rules formulated as follows: if a class defines any of the 5 then it should define all of them. Well, yes, and no. The reason behind it is: if a class defines any of the 5 then that is a strong indicator that the class has to manage a resource, in which case it should define all 5. So, for instance if you define a destructor to free a resource then the class is in the first category and should implement all 5, but if you define a destructor just to add some debug statements or do some logging, or because the class is polymorphic then the class is not the first category, so you don't need to define all 5.

If your class is in the second category and you define at least one of the 5, then you should explicitly =default the rest of the 5. That is because the rules on when cpy/move ctors/assignments are implicitly declared are a bit complicated. E.g. defining a dtor prevents the implicit declaration of move ctor & assigment


// because I have a custom constructor, I assume I need to also
// define copy constructors, move constructors, and a destructor.

Wrong. A custom constructor is not part of the 5.


All of your classes should follow the rule of 0, as neither of them manages resources.

Actually it's pretty rare for a user code to need to implement a rule of 5 class. Usually that's implemented in a library. So, as a user, almost always for to the rule of 0.


The default copy/move ctors/assignments do the expected thing: the copy ones will copy each member, and the move ones will move each member. Well, in the presence of members that aren't movable, or have only some of the 5, or have some of the 5 deleted, then the rules go a little more complex, but there are no unexpected behaviors. The default are good.

Is this a proper application of the rule of five with abstract base class and unique_ptr member?

I'd like to offer an alternative approach. Not the Scary Rule of Five, but the Pleasant Rule of Zero, as @Tony The Lion has already suggested. A full implementation of my proposal has been coded by se­ve­ral people, and there's a fine version in @R. Martinho Fernandes's library, but I'll present a simplified version.

First, let's recap:

The Rule of Zero: Don't write a copy- or move-constructor, a copy- or move-assignment ope­ra­tor, or a destructor. Instead, compose your class of components which handle a single responsibility and encap­su­late the desired behaviour for the individual resource in question.

There's an obvious caveat: When you design the single-responsibility class, you must of course obey:

The Rule of Five: If you write any one of copy- or move-constructor, copy- or move-assignment ope­ra­tor, or destructor, you must implement all five. (But the "five" functions needed by this rule are actually: Destructor, Copy-Const, Move-Const, Assignment and Swap.)

Let's do it. First, your consumer:

struct X;

struct Base
{
std::vector<value_ptr<X>> v;
};

struct Derived : Base
{
};

Note that both Base and Derived obey the Rule of Zero!

All we need to do is implement value_ptr. If the pointee is non-polymorphic, the following will do:

template <typename T>
class value_ptr
{
T * ptr;
public:
// Constructors
constexpr value_ptr() noexcept : ptr(nullptr) { }
constexpr value_ptr(T * p) noexcept : ptr(p) { }

// Rule of Five begins here:
~value_ptr() { ::delete ptr; }
value_ptr(value_ptr const & rhs) : ptr(rhs.ptr ? ::new T(*rhs.ptr) : nullptr) { }
value_ptr(value_ptr && rhs) noexcept : ptr(rhs.ptr) { rhs.ptr = nullptr; }
value_ptr & operator=(value_ptr rhs) { swap(rhs); return *this; }
void swap(value_ptr & rhs) noexcept { std::swap(rhs.ptr, ptr); }

// Pointer stuff
T & operator*() const noexcept { return *ptr; }
T * operator->() const noexcept { return ptr; }
};

template <typename T, typename ...Args>
value_ptr<T> make_value(Args &&... args)
{
return value_ptr<T>(::new T(std::forward<Args>(args)...));
}

If you would like smart pointer that handles polymorphic base class pointers, I suggest you demand that your base class provide a virtual clone() function, and that you implement a clone_ptr<T>, whose copy constructor would be like this:

clone_ptr(clone_ptr const & rhs) : ptr(rhs.ptr ? rhs.ptr->clone() : nullptr) { }

Is the Rule of 5 (for constructors and destructors) outdated?

The full name of the rule is the rule of 3/5/0.

It doesn't say "always provide all five". It says that you have to either provide the three, the five, or none of them.

Indeed, more often than not the smartest move is to not provide any of the five. But you can't do that if you're writing your own container, smart pointer, or a RAII wrapper around some resource.

Rule of five for derived classes

For the derived class, you can reuse the base part of the copy and move constructors/assignment operators defined in the base class and only add what's added in the derived class.

Before I show that, I've made a few corrections to the base class since it doesn't actually move anything. I also recommend not implementing this using getter methods. Go straight for the resource you need.

#include <utility>

class A {
int a;
std::vector<int> k;
int *z;

public:
// getters removed - not needed for this example

A(int a, std::vector<int> k, int* z) : a(a), k(std::move(k)), z(z) {}

// rule of 5 start
A(const A& Other) : a(Other.a), k(Other.k), z(Other.z) {
// note that z can't be an owning pointer in this example
}

// actually move the content of the vector and exchange the pointer
// in the moved from object with a nullptr
A(A&& Other) :
a(Other.a), k(std::move(Other.k)), z(std::exchange(Other.z, nullptr))
{}

A& operator=(const A& Other) {
if(this == &Other) return *this; // don't do anything if self-assigning
return *this = A(Other); // this uses the move assignment operator
}

A& operator=(A&& Other) {
if(this == &Other) return *this;
a = Other.a;
std::swap(k, Other.k); // swap content, let Other destroy it
std::swap(z, Other.z); // not important since it's not an owning pointer
return *this;
}

~A() = default; // ... as z is not an owning pointer
// rule of 5 end
};

Now to the answer-part - How to reuse what you already defined in A:

class B : public A {
char* t;

public:

B(int a, std::vector<int> k, int* z, char* t) : A(a, std::move(k), z), t(t) {
// again, t can't be an owning pointer
}

// rule of 5 start
B(const B& Other) : A(Other), t(Other.t) {} // copies the A part + t

// similar
B(B&& Other) : A(std::move(Other)), t(std::exchange(Other.t, nullptr)) {}

B& operator=(const B& Other) {
if(this == &Other) return *this;

// use the base class operator by specifying it explicitly
A::operator=(Other); // use the base class' copy assignment operator
t = Other.t; // just add t
return *this;
}

// similar as in copy assignment
B& operator=(B&& Other) {
if(this == &Other) return *this;
A::operator=(std::move(Other)); // use the base class' move assignment operator
std::swap(t, Other.t); // and just swap t
return *this;
}

~B() = default;
// rule of 5 end
};


Related Topics



Leave a reply



Submit