The Rule of Zero
In relation to c ++ 03, there is a “rule of three”, with the advent of c ++ 11 it was transformed into a “rule of 5”. Although these rules are essentially nothing more than informal recommendations for designing your own data types, they are nevertheless often useful. The Rule of Zero continues these recommendations. In this post I will remind you of what, in fact, the first 2 rules, and also try to explain the idea behind the “zero rule”.
All the rules mentioned above are written mainly (but not always) for situations when an object of our class owns some resource (handler, pointer to a resource) and you need to somehow decide what will happen to this handler and to the resource itself when copying / moving our object.
By default, if we do not declare any of the “special” functions (copy constructor, assignment operator, destructor, etc.), the compiler will generate their code automatically. At the same time, they will behave as expected. For example, the copy constructor will try to copy non- POD class members by calling their corresponding copy constructors and copy the POD members bit by bittypes. This behavior is quite acceptable for simple classes containing all of their members in themselves.
In the case of large complex classes, or classes, in which the handler of the external resource acts as a member, the behavior implemented by the default compiler may no longer suit us. Fortunately, we can independently determine special functions by implementing the resource ownership strategy we need in a given situation. Conventionally, there are several basic such strategies:
1. prohibition of copying and moving;
2. copying the shared resource along with the handler ( deep copy );
3. prohibition of copying, but permission to move;
4. joint ownership (regulated, for example, by reference counting).
So the “rule of three” and “rule of 5” say that in the general case, if there is a need to independently determine one of the operations of copying, moving or destroying our object in accordance with one of the selected strategies, then most likely for correct operation will determine all other functions too.
Why this is so is easy to see in the following example. Suppose a member of our class is a pointer to an object on the heap.
The default destructor in this situation does not suit us, since it destroys only the counter_ pointer itself, but not what it points to. Define a destructor.
But what now happens when you try to copy an object of our class? The default copy constructor is called, which honestly copies the pointer, and as a result, we will have 2 objects that own a pointer to the same resource. This is bad for obvious reasons. So we need to define our own copy constructor, assignment operator, etc.
So what's the deal? Let's always define all 5 “special” functions and everything will be ok. It is possible, but, frankly, quite tiring and fraught with errors. Then let us determine only those that are really necessary in the current situation, and let the rest be generated by the compiler? This is also an option, but firstly, the “situation” in which our code is used may well change without our knowledge, and our class will be unable to work in the new conditions, and secondly there are special (and, it seems to me, rather confusing) rules that suppress compiler generation spec. functions. For example, “move functions will not be implicitly generated by the compiler if there is at least one explicitly declared function from 5k” or “copy functions will not be generated if there is at least one explicitly declared move function”.
One of the possible solutions was voiced by Martino Fernandez in the form of the “zero rule” and can be briefly formulated as follows: “do not define any of the 5k functions on your own, instead assign responsibility for the ownership of resources to classes specially invented for this”. And such special classes are already in the standard library. These are std :: unique_ptr and std :: shared_ptr . Due to the fact that when using these classes, it is possible to set custom deleters's, with the help of them you can implement most of the ownership strategies described above (at least the most useful). For example, if a class owns an object for which joint ownership does not make sense or is even harmful (file descriptor, mutex, stream, etc.), wrap this object in std :: unique_ptr with the corresponding deleter. Now the object of our class cannot be copied (only moved), and the correct destruction of the resource will automatically be ensured when our object is destroyed. If the semantics of the stored handler allows for joint ownership of the resource, then we use shared_ptr . As an example, the above example with a pointer to a counter is suitable.
Wait ... But in situations with polymorphic inheritance, we simply must declare a virtual destructor to ensure the correct destruction of derived objects. It turns out the “zero rule" is not applicable here? Not certainly in that way. Shared_ptr will help us in this situation. The fact is that deleter shared_ptr 'remembers' the actual type of the pointer stored in it.
If you are confused by the shared_ptr overhead, or if you want to ensure exclusive ownership of the pointer to your polymorphic object, you can also wrap it in unique_ptr, but then you have to write your own custom deleter .
The latter method is fraught with certain problems. For multiple inheritance, you have to write 2 (or more) different deleters, it is also possible to move one smart pointer from another, despite the fact that the implementation of deleters can be different.
So, the “zero rule” is another approach to the resource management mechanism, but like any other C ++ idioms, you can’t use it thoughtlessly. In each specific situation it is necessary to decide separately whether it makes sense to apply it. In the links below there is an article by Scott Meyers on this topic.
flamingdangerzone.com/cxx11/2012/08/15/rule-of-zero.html
scottmeyers.blogspot.ru/2014/03/a-concern-about-rule-of-zero.html
stackoverflow.com/questions/4172722/ what-is-the-rule-of-three
stackoverflow.com/questions/4782757/rule-of-three-becomes-rule-of-five-with-c11
Motivation
All the rules mentioned above are written mainly (but not always) for situations when an object of our class owns some resource (handler, pointer to a resource) and you need to somehow decide what will happen to this handler and to the resource itself when copying / moving our object.
By default, if we do not declare any of the “special” functions (copy constructor, assignment operator, destructor, etc.), the compiler will generate their code automatically. At the same time, they will behave as expected. For example, the copy constructor will try to copy non- POD class members by calling their corresponding copy constructors and copy the POD members bit by bittypes. This behavior is quite acceptable for simple classes containing all of their members in themselves.
Ownership strategies
In the case of large complex classes, or classes, in which the handler of the external resource acts as a member, the behavior implemented by the default compiler may no longer suit us. Fortunately, we can independently determine special functions by implementing the resource ownership strategy we need in a given situation. Conventionally, there are several basic such strategies:
1. prohibition of copying and moving;
2. copying the shared resource along with the handler ( deep copy );
3. prohibition of copying, but permission to move;
4. joint ownership (regulated, for example, by reference counting).
“Rule of Three” and “Rule of Five”
So the “rule of three” and “rule of 5” say that in the general case, if there is a need to independently determine one of the operations of copying, moving or destroying our object in accordance with one of the selected strategies, then most likely for correct operation will determine all other functions too.
Why this is so is easy to see in the following example. Suppose a member of our class is a pointer to an object on the heap.
class my_handler {
public:
my_handler(int c) : counter_(new int(c)) {}
private:
int* counter_;
};
The default destructor in this situation does not suit us, since it destroys only the counter_ pointer itself, but not what it points to. Define a destructor.
my_handler::~my_handler() {delete counter_;}
But what now happens when you try to copy an object of our class? The default copy constructor is called, which honestly copies the pointer, and as a result, we will have 2 objects that own a pointer to the same resource. This is bad for obvious reasons. So we need to define our own copy constructor, assignment operator, etc.
So what's the deal? Let's always define all 5 “special” functions and everything will be ok. It is possible, but, frankly, quite tiring and fraught with errors. Then let us determine only those that are really necessary in the current situation, and let the rest be generated by the compiler? This is also an option, but firstly, the “situation” in which our code is used may well change without our knowledge, and our class will be unable to work in the new conditions, and secondly there are special (and, it seems to me, rather confusing) rules that suppress compiler generation spec. functions. For example, “move functions will not be implicitly generated by the compiler if there is at least one explicitly declared function from 5k” or “copy functions will not be generated if there is at least one explicitly declared move function”.
“The rule of zero”
One of the possible solutions was voiced by Martino Fernandez in the form of the “zero rule” and can be briefly formulated as follows: “do not define any of the 5k functions on your own, instead assign responsibility for the ownership of resources to classes specially invented for this”. And such special classes are already in the standard library. These are std :: unique_ptr and std :: shared_ptr . Due to the fact that when using these classes, it is possible to set custom deleters's, with the help of them you can implement most of the ownership strategies described above (at least the most useful). For example, if a class owns an object for which joint ownership does not make sense or is even harmful (file descriptor, mutex, stream, etc.), wrap this object in std :: unique_ptr with the corresponding deleter. Now the object of our class cannot be copied (only moved), and the correct destruction of the resource will automatically be ensured when our object is destroyed. If the semantics of the stored handler allows for joint ownership of the resource, then we use shared_ptr . As an example, the above example with a pointer to a counter is suitable.
Wait ... But in situations with polymorphic inheritance, we simply must declare a virtual destructor to ensure the correct destruction of derived objects. It turns out the “zero rule" is not applicable here? Not certainly in that way. Shared_ptr will help us in this situation. The fact is that deleter shared_ptr 'remembers' the actual type of the pointer stored in it.
struct base {virtual void foo() = 0;};
struct derived : base {void foo() override {...}};
base* bad = new derived;
delete bad; // Плохо! Нет виртуального деструктора в base
{
...
std::shared_ptr good = std::make_shared();
} // Хорошо! shared_ptr при разрушении вызовет правильный деструктор.
If you are confused by the shared_ptr overhead, or if you want to ensure exclusive ownership of the pointer to your polymorphic object, you can also wrap it in unique_ptr, but then you have to write your own custom deleter .
typedef std::unique_ptr base_ptr;
base_ptr good{new base, [](void* p){delete static_cast(p);}};
The latter method is fraught with certain problems. For multiple inheritance, you have to write 2 (or more) different deleters, it is also possible to move one smart pointer from another, despite the fact that the implementation of deleters can be different.
So, the “zero rule” is another approach to the resource management mechanism, but like any other C ++ idioms, you can’t use it thoughtlessly. In each specific situation it is necessary to decide separately whether it makes sense to apply it. In the links below there is an article by Scott Meyers on this topic.
References
flamingdangerzone.com/cxx11/2012/08/15/rule-of-zero.html
scottmeyers.blogspot.ru/2014/03/a-concern-about-rule-of-zero.html
stackoverflow.com/questions/4172722/ what-is-the-rule-of-three
stackoverflow.com/questions/4782757/rule-of-three-becomes-rule-of-five-with-c11