What Is the Rule of Three

What is The Rule of Three?

Introduction

C++ treats variables of user-defined types with value semantics.
This means that objects are implicitly copied in various contexts,
and we should understand what "copying an object" actually means.

Let us consider a simple example:

class person
{
std::string name;
int age;

public:

person(const std::string& name, int age) : name(name), age(age)
{
}
};

int main()
{
person a("Bjarne Stroustrup", 60);
person b(a); // What happens here?
b = a; // And here?
}

(If you are puzzled by the name(name), age(age) part,
this is called a member initializer list.)

Special member functions

What does it mean to copy a person object?
The main function shows two distinct copying scenarios.
The initialization person b(a); is performed by the copy constructor.
Its job is to construct a fresh object based on the state of an existing object.
The assignment b = a is performed by the copy assignment operator.
Its job is generally a little more complicated,
because the target object is already in some valid state that needs to be dealt with.

Since we declared neither the copy constructor nor the assignment operator (nor the destructor) ourselves,
these are implicitly defined for us. Quote from the standard:

The [...] copy constructor and copy assignment operator, [...] and destructor are special member functions.
[ Note: The implementation will implicitly declare these member functions
for some class types when the program does not explicitly declare them.

The implementation will implicitly define them if they are used. [...] end note ]
[n3126.pdf section 12 §1]

By default, copying an object means copying its members:

The implicitly-defined copy constructor for a non-union class X performs a memberwise copy of its subobjects.
[n3126.pdf section 12.8 §16]

The implicitly-defined copy assignment operator for a non-union class X performs memberwise copy assignment
of its subobjects.
[n3126.pdf section 12.8 §30]

Implicit definitions

The implicitly-defined special member functions for person look like this:

// 1. copy constructor
person(const person& that) : name(that.name), age(that.age)
{
}

// 2. copy assignment operator
person& operator=(const person& that)
{
name = that.name;
age = that.age;
return *this;
}

// 3. destructor
~person()
{
}

Memberwise copying is exactly what we want in this case:
name and age are copied, so we get a self-contained, independent person object.
The implicitly-defined destructor is always empty.
This is also fine in this case since we did not acquire any resources in the constructor.
The members' destructors are implicitly called after the person destructor is finished:

After executing the body of the destructor and destroying any automatic objects allocated within the body,
a destructor for class X calls the destructors for X's direct [...] members
[n3126.pdf 12.4 §6]

Managing resources

So when should we declare those special member functions explicitly?
When our class manages a resource, that is,
when an object of the class is responsible for that resource.
That usually means the resource is acquired in the constructor
(or passed into the constructor) and released in the destructor.

Let us go back in time to pre-standard C++.
There was no such thing as std::string, and programmers were in love with pointers.
The person class might have looked like this:

class person
{
char* name;
int age;

public:

// the constructor acquires a resource:
// in this case, dynamic memory obtained via new[]
person(const char* the_name, int the_age)
{
name = new char[strlen(the_name) + 1];
strcpy(name, the_name);
age = the_age;
}

// the destructor must release this resource via delete[]
~person()
{
delete[] name;
}
};

Even today, people still write classes in this style and get into trouble:
"I pushed a person into a vector and now I get crazy memory errors!"
Remember that by default, copying an object means copying its members,
but copying the name member merely copies a pointer, not the character array it points to!
This has several unpleasant effects:

  1. Changes via a can be observed via b.
  2. Once b is destroyed, a.name is a dangling pointer.
  3. If a is destroyed, deleting the dangling pointer yields undefined behavior.
  4. Since the assignment does not take into account what name pointed to before the assignment,
    sooner or later you will get memory leaks all over the place.

Explicit definitions

Since memberwise copying does not have the desired effect, we must define the copy constructor and the copy assignment operator explicitly to make deep copies of the character array:

// 1. copy constructor
person(const person& that)
{
name = new char[strlen(that.name) + 1];
strcpy(name, that.name);
age = that.age;
}

// 2. copy assignment operator
person& operator=(const person& that)
{
if (this != &that)
{
delete[] name;
// This is a dangerous point in the flow of execution!
// We have temporarily invalidated the class invariants,
// and the next statement might throw an exception,
// leaving the object in an invalid state :(
name = new char[strlen(that.name) + 1];
strcpy(name, that.name);
age = that.age;
}
return *this;
}

Note the difference between initialization and assignment:
we must tear down the old state before assigning to name to prevent memory leaks.
Also, we have to protect against self-assignment of the form x = x.
Without that check, delete[] name would delete the array containing the source string,
because when you write x = x, both this->name and that.name contain the same pointer.

Exception safety

Unfortunately, this solution will fail if new char[...] throws an exception due to memory exhaustion.
One possible solution is to introduce a local variable and reorder the statements:

// 2. copy assignment operator
person& operator=(const person& that)
{
char* local_name = new char[strlen(that.name) + 1];
// If the above statement throws,
// the object is still in the same state as before.
// None of the following statements will throw an exception :)
strcpy(local_name, that.name);
delete[] name;
name = local_name;
age = that.age;
return *this;
}

This also takes care of self-assignment without an explicit check.
An even more robust solution to this problem is the copy-and-swap idiom,
but I will not go into the details of exception safety here.
I only mentioned exceptions to make the following point: Writing classes that manage resources is hard.

Noncopyable resources

Some resources cannot or should not be copied, such as file handles or mutexes.
In that case, simply declare the copy constructor and copy assignment operator as private without giving a definition:

private:

person(const person& that);
person& operator=(const person& that);

Alternatively, you can inherit from boost::noncopyable or declare them as deleted (in C++11 and above):

person(const person& that) = delete;
person& operator=(const person& that) = delete;

The rule of three

Sometimes you need to implement a class that manages a resource.
(Never manage multiple resources in a single class,
this will only lead to pain.)
In that case, remember the rule of three:

If you need to explicitly declare either the destructor,
copy constructor or copy assignment operator yourself,
you probably need to explicitly declare all three of them.

(Unfortunately, this "rule" is not enforced by the C++ standard or any compiler I am aware of.)

The rule of five

From C++11 on, an object has 2 extra special member functions: the move constructor and move assignment. The rule of five states to implement these functions as well.

An example with the signatures:

class person
{
std::string name;
int age;

public:
person(const std::string& name, int age); // Ctor
person(const person &) = default; // 1/5: Copy Ctor
person(person &&) noexcept = default; // 4/5: Move Ctor
person& operator=(const person &) = default; // 2/5: Copy Assignment
person& operator=(person &&) noexcept = default; // 5/5: Move Assignment
~person() noexcept = default; // 3/5: Dtor
};

The rule of zero

The rule of 3/5 is also referred to as the rule of 0/3/5. The zero part of the rule states that you are allowed to not write any of the special member functions when creating your class.

Advice

Most of the time, you do not need to manage a resource yourself,
because an existing class such as std::string already does it for you.
Just compare the simple code using a std::string member
to the convoluted and error-prone alternative using a char* and you should be convinced.
As long as you stay away from raw pointer members, the rule of three is unlikely to concern your own code.

Rule of Three in C++

If you know that the copy constructor won't be used, you can express that by making it private and unimplemented, thus:

class C
{
private:
C(const C&); // not implemented
};

(in C++11 you can use the new = delete syntax). That said, you should only do that if you're absolutely sure it will never be needed. Otherwise, you might be better off implementing it. It's important not to just leave it as is, as in that case the compiler will provide a default memberwise copy constructor that will do the wrong thing - it's a problem waiting to happen.

To some extent it depends on what the class is going to be used for - if you're writing a class that's part of a library, for instance, it makes much more sense to implement the copy constructor for consistency reasons. You have no idea a priori how your class is going to be used.

Exception to the Rule of Three?

Don't worry so much about the "Rule of Three". Rules aren't there to be obeyed blindly; they're there to make you think. You've thought. And you've concluded that the destructor wouldn't do it. So don't write one. The rule exists so that you don't forget to write the destructor, leaking resources.

All the same, this design creates a potential for B::ap to be wrong. That's an entire class of potential bugs that could be eliminated if these were a single class, or were tied together in some more robust way.

Am I violating Rule of three?

The rule of three is about dealing with all the Big Three, but that does not necessarily mean you'll have to define them if you don't want to. Either you provide them or you forbid them. What you shouldn't do is ignore them.


So I have defined only destructor, but not copy constructor and copy operator.

Am I violating Rule of three?

Yes, you are in violation of the rule. The compiler will generate a copy constructor and copy assignment operator, and since you allocate memory in the constructor and release in the destructor, these copies will have wrong semantics: they'll copy the pointers, and you will have two classes aliasing the same memory. The assignment won't even release the old memory, and simply overwrite the pointer.

Is this a problem?

If, like you imply, you don't make copies or assign to instances of those classes, nothing will go wrong. However, it's better to be on the safe side and declare (and don't even bother defining) the copy constructor and copy assignment operator private, so you don't invoke them accidentally.

In C++11 you can use the = delete syntax instead:

T(T const&) = delete; // no copy constructor
T& operator=(T const&) = delete; // no copy assignment

Implementation of Rule of Three gone wrong

The solution can be derived by laying out the exact sequence of events, e.g.: More print outs and testing, what parameters are called when:

Starting in: v1 = v2;

  1. v2 calls copy constructor with the argument other (whatever other is), in particular: its int* k does not point to valid memory. For simplicity let's call this new Vector2 v3.
  2. The copy assignment constructor is called now with v3.
  3. Then we begin swap.

The error actually arises within the copy constructor, as v3 is not initialised properly in step 1 .

Step 2 and step 3 are basically "hiding", transferring the error from v3 onto v1.

The interesting question now is, how is v3 actually generated? Not by the default constructor!

Rule of Three. Copy Constructor, Assignment Operator Implementation

Q0. No, this calls the copy constructor only. That's a pretty big misunderstanding, objects are only ever constructed once.

Q1. That's correct

Q2. Presumably you are meant to store the array size in size. E.g.

IntPart::IntPart()
{
Counts = new int[101] (); // allocate all to 0s
numParts = 0;
size = 101; // save array size
}

If you don't store the array size somewhere, your copy constructor will be impossible to write.

Q3. I would look up the copy and swap idiom. This lets you write the assignment operator using the copy constructor.

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.

Why is a non-default constructor NOT considered in the Rule of Three?

Why is a non-default constructor not considered as one of them? When there is any resource managed in the class, programmer has to define a non-default constructor anyway.

That is not necessarily true. The constructor might not aquire any resource. Other function(s) might aquire them as well. In fact, there can be many functions (including the constructor(s) themselves) which might aquire resources. For example, in case of std::vector<T>, it is resize() and reserve() which aquire resources. So think of constructor(s) just like other function(s) which might aquire resources.

The idea of this rule is that when you make a copy, the default-copy code generated by the compiler wouldn't work. Hence you need to write the copy-semantic yourself. And since the class manages resources (it doesn't matter which function(s) aquire it), the destructor must release it, because the destructor is guaranteed to be executed, for a fully constructed object. Hence you've to define the destructor as well. And in C++11, you've to implement move-semantics as well. The logical argument for move-semantic is same as that of copy-semantics, except that in move semantic, you changed the source as well. Move-semantic is much like organ-donor; when you give your organ to other, you don't own it anymore.



Related Topics



Leave a reply



Submit