📜 ⬆️ ⬇️

"Rule of zero"

With reference to c ++ 03 there is a “rule of three”, with the advent of c ++ 11 it has been transformed into a “rule of 5”. And although these rules are essentially no more than informal recommendations for designing your own data types, they are nevertheless often useful. The “zero rule” continues a series of 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”.

Motivation


All the rules mentioned above are written mainly (but not always) for situations when an object of our class owns a resource (handler, pointer to a resource) and needs to somehow decide what will happen to this handler and the resource itself when copying / moving our facility.
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. In this case, they will behave in general, as expected. For example, the copy constructor will try to copy non- POD class members by calling their corresponding copy constructor and copy the POD type members bit by bit. This behavior is perfectly acceptable for simple classes that contain all their members in themselves.

Ownership strategies


In the case of large complex classes, or classes, as a member of which is the handler of an external resource, the behavior implemented by the compiler by default may no longer suit us. Fortunately, we can independently define special functions by implementing the strategy of owning a resource that is necessary in this situation. Conventionally, there are several basic such strategies:
1. prohibition of copying and moving;
2. copying a shared resource together with a 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” indicate that, in general, if it became necessary 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 you need 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 will destroy only the counter_ pointer itself, but not what it indicates. Determine the destructor.

 my_handler::~my_handler() {delete counter_;} 


But what happens when I try to copy an object of our class? The default copy constructor, which honestly copies the pointer, will be called and as a result we will have 2 objects that own the 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 tedious and fraught with errors. Then let's define only those that are really needed 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 not be able to work in the new conditions, and secondly, there are special (and, I think, rather confusing) rules that suppress generation by the compiler functions. For example, “the transfer functions will not be implicitly generated by the compiler if there is at least one explicitly declared function of 5ki” or “the copy functions will not be generated if there is at least one explicitly declared transfer function”.

“Zero rule”


One of the possible outcomes was voiced by Martino Fernandez in the form of a “rule of zero” and can be briefly formulated as follows: “do not define any of the functions of 5ki yourself, instead, take care of the ownership of resources specifically designed for this class”. 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 specify custom deleters , you can use them to implement most of the 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 even harmful (file descriptor, mutex, stream, etc.), wrap this object in std :: unique_ptr with the corresponding deleter. Now the object of our class can not be copied (only moved), and the correct destruction of the resource will be automatically 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 will do.
Wait ... But in situations with polymorphic inheritance, we simply have to declare a virtual destructor in order to ensure the correct destruction of derived objects. Is the “rule of zero” not applicable here? Not certainly in that way. Shared_ptr will help us in this situation. The fact is that deleter shared_ptr "and" remembers "the real type of 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<base> good = std::make_shared<derived>(); } // ! shared_ptr     . 


If you are confused by the shared_ptr overhead or if you want to provide exclusive possession of the pointer to your polymorphic object, you can wrap it in unique_ptr, but then you have to write your custom deleter .

 typedef std::unique_ptr<base, void(*)(void*)> base_ptr; base_ptr good{new base, [](void* p){delete static_cast<derived*>(p);}}; 


The latter method is fraught with certain problems. For multiple inheritance, you will have to write 2 (or more) different deleters, and you also have the opportunity to move one smart pointer from another, despite the fact that the implementation of deleters may be different.

So, the “rule of zero” is another approach to the resource management mechanism, but like any other C ++ idioms, you can’t use it without thinking. In each particular 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.

Links

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

Source: https://habr.com/ru/post/221033/


All Articles