📜 ⬆️ ⬇️

SI ++: The Law of the Big Two

original: www.artima.com/cppsource/bigtwo.html
authors: Bjarn Karlsson and Matthew Wilson (Bjorn Karlsson and Matthew Wilson)
October 1, 2004

Summary

Welcome to the first issue of Smart Pointers in a monthly column written exclusively for The C ++ Source. In this column, programmers Bjarn and Matthew will carefully consider C ++ idioms, tricks and powerful techniques. So that you do not get bogged down in all the complexity of the serious topics under consideration, we will occasionally dilute them with programmer jokes. So, who said that there are no such things as a free lunch? In this issue, the authors will revise the Big Three Law, and will explain which of these three magical components are often not needed.

That's right, you correctly read the title of the article. A well-known and very important rule, known as the Big Three Law, states that if you ever need a (non-virtual) copy constructor, a copy assignment operator, or destructor, then you will most likely also need to implement all the rest of them. This set of special functions, the copy constructor, the copying assignment operator, and the destructor, is proudly called the “Big Three” C ++ programmers around the world; This name was given to him by Marshall Cline in the C ++ FAQ. We argue that one of these three special functions will not be a problem for most classes. And this article will explain why this is so.
')
Origin

To understand what is generally understood by the “Big Three Law”, let's see what happens when we dynamically allocate memory for resources in a class (SomeResource * p_ in the code below):

class Example {
SomeResource * p_;
public:
Example (): p_ (new SomeResource ()) {}
};

Now, because we allocated memory for the resource in the constructor; we will need to free it in the destructor, i.e. add:

~ Example () {
delete p_;
}

That's it, it's ok, right? Wrong! As soon as someone decides to create a class using the copy constructor, everything will go to hell. The reason is that the copy constructor generated by the compiler will simply create a copy of the p_ pointer; he has no way of knowing that he also needs to allocate memory for the new SomeResource. Thus, when the first object of the class Example is deleted, its destructor will release p_. Further use of resources in another object of the class Example (including deleting this resource in its destructor, naturally) will lead to a repeated attempt to free the already freed memory, because an object of type SomeResource no longer exists. Check it out:

class Example {
SomeResource * p_;
public:
Example (): p_ (new SomeResource ()) {
std :: cout << "Creating Example, allocating SomeResource! \ n";
}
~ Example () {
std :: cout << "Deleting Example, freeing SomeResource! \ n";
delete p_;
}
};

int main () {
Example e1;
Example e2 (e1);
}

Running this program is guaranteed to lead to disastrous results. Run:

C: \ projects> bigthree.exe
Creating Example, allocating SomeResource!
Deleting Example, freeing someresource!
Deleting Example, freeing someresource!
6 [main] bigthree 2664 handle_exceptions:
Exception: STATUS_ACCESS_VIOLATION
1176 [main] bigthree 2664 open_stackdumpfile:
Dumping stack trace to bigthree.exe.stackdump

It is clear that we will need to take care of the copy constructor, which would correctly copy SomeResource:

Example (const Example & other): p_ (new SomeResource (* other.p_)) {}

Assuming that SomeResource has an available copy constructor, this will slightly improve the situation; but still this program will crash down as soon as someone decides to try to assign an object of class Example to another object of the same class:

int main () {
Example e1;
Example e2;
e2 = e1;
}

The consequences will be more tragic in such a case; take a look at the conclusion:

C: \ projects> bigthree.exe
Creating Example, allocating SomeResource!
Creating Example, allocating SomeResource!
Deleting Example, freeing someresource!
Deleting Example, freeing someresource!
5 [main] bigthree 3780 handle_exceptions:
Exception: STATUS_ACCESS_VIOLATION
1224 [main] bigthree 3780 open_stackdumpfile:
Dumping stack trace to bigthree.exe.stackdump

As we see, memory was allocated for two objects of type SomeResource, and both of them were deleted. So what's the problem? Well, the problem is that both objects of the Example class point to the same object of type SomeResource! This is due to the automatically generated assignment copy operator, which only knows how to equate the pointer to SomeResource. Thus, we also need to implement the appropriate copy copy operator in order to deal with the copy constructor:

Example & operator = (const Example & other) {
// Self assignment?
if (this == & other)
return * this;

* p _ = * other.p_; // use SomeResource :: operator =
return * this;
}

You will notice that this statement first checks for a self-assignment attempt, and in this case simply returns * this. Given the exception safety, the copy copy operator provides its basic guarantees.

Now the program behaves correctly! The lesson shown above is exactly what is called the “Big Three Law”: as soon as you need a non-trivial destructor, make sure that the copy constructor and the copy assignment operator also work correctly. In most cases, this is checked manually during their implementation.

Prohibition of the use of the Copy Constructor and the Assignment Copy Operator

It should be noted that there are cases where the copy constructor and the copy assignment operator do not make sense in the class. In such cases, the usual thing is to simply ban them using the general idiom of declaring them in the private section, as in the example below:

class SelfishBeastie
{
...

private:
SelfishBeastie (const SelfishBeastie &);
SelfishBeastie & operator = (const SelfishBeastie &);
};

Another option would be to use the class boost :: noncopyable from the Boost library; inheritance from such a class will be quite correct, since This class explains that it does not support copying and assignment (at least for those who are familiar with noncopyable!).

class SelfishBeastie
: boost :: noncopyable
{
...

Another way to prohibit the copy constructor and the copy assignment operator is to change the type of one or more class members to a link or const (or to a const link, for greater caution) - this effectively cuts off the ability of the compiler to generate these special functions members. As Matthew wrote in his book “Imperfect C ++”, this is an undesirable way to create a non-replicating class, since This method breaks interaction with the class interface for its users. However, this is a great way to achieve design solutions; thus, it is a mechanism for “communicating” initial decisions on the interface with future implementations of this class, and even more so for documenting the semantics of the class. (Of course, with such a technique, all class constructors will need to initialize the members of the link (instead of overwriting them in the body of the constructor), which in itself is a good idea).

Big Three Are Not Enough

Although we have come to the way of creating a workable and correct Example class, it is easy to get confused in exceptional cases that may be. Let's add another pointer to SomeResource in our Example class, like this:

class Example {
SomeResource * p_;
SomeResource * p2_;
public:
Example ():
p_ (new SomeResource ()),
p2_ (new SomeResource ()) {
std :: cout << "Creating Example, allocating SomeResource! \ n";
}

Example (const Example & other):
p_ (new SomeResource (* other.p_)),
p2_ (new SomeResource (* other.p2_)) {}

Example & operator = (const Example & other) {
// Self assignment?
if (this == & other)
return * this;

* p _ = * other.p_;
* p2 _ = * other.p2_;
return * this;
}

~ Example () {
std :: cout << "Deleting Example, freeing SomeResource! \ n";
delete p_;
delete p2_;
}
};

Now let us try to suppose what will happen when, in the process of creating an object of class Example, a second object of type SomeResource (pointed to by p2_) throws an exception. The object pointed to by p_ has already been allocated in memory, but the destructor will not be called anyway! The reason is that, from the point of view of the compiler, an object of class Example has never existed, because its constructor was not completed. This, unfortunately, means that Example is not exception-safe, due to the possibility of resource leaks.

To make it safe, you can alternatively put initialization outside of ctor-initializer, for example, like this:

Example (): p_ (0), p2_ (0)
{
try {
p_ = new SomeResource ();
p2_ = new SomeResource (“H”, true);
std :: cout << "Creating Example, allocating SomeResource! \ n";
}
catch (...) {
delete p2_;
delete p_;
throw;
}
}

Although we coped with the problem of exception-safety, this is not an appropriate solution, because we, C ++ programmers, prefer initialization rather than assignment. As you will soon see, an old and reliable way will come to our aid.

RAII Saves the Day

In our case, we could use the ubiquitous technique RAII (Resource Acquisition Is Initialization, Resource Acquisition / Isolation There Is Initialization), because we are looking for exactly what is essentially the essence of RAII, namely, the property that the constructor of a local object controls the allocation of resources, and its destructor releases them.
Using this idiom it is impossible to forget to free up resources; Also, this idiom does not require to remember about the cunning difficulties with which we fought manually in the class Example. A simple wrapper class, designed primarily to add RAII functionality to simple classes like SomeResource, would look like this:

template class RAII {
T * p_;
public:
explicit RAII (T * p): p_ (p) {}

~ RAII () {
delete p_;
}

void reset (T * p) {
delete p_;
p_ = p;
}

T * get () const {
return p_;
}

T & operator * () const {
return * p_;
}

void swap (RAII & other) {
std :: swap (p_, other.p_);
}

private:
RAII (const RAII & other);
RAII & operator = (const RAII & other);
};

The only tasks that this class has are storing the pointer, returning it on request, and correctly freeing the memory it points to when destroyed. Using this class will greatly simplify the Example class; both now and with further changes.

class Example {
RAII p_;
RAII p2_;
public:
Example ():
p_ (new SomeResource ()),
p2_ (new SomeResource ()) {}

Example (const Example & other)
: p_ (new SomeResource (* other.p_)),
p2_ (new SomeResource (* other.p2_)) {}

Example & operator = (const Example & other) {
// Self assignment?
if (this == & other)
return * this;

* p _ = * other.p_;
* p2 _ = * other.p2_;
return * this;
}

~ Example () {
std :: cout << "Deleting Example, freeing SomeResource! \ n";
}
};

We essentially returned to where we started from, using the original version of the class, which did not care about exception safety.

This brings us to a very important point - the destructor now does no work other than displaying a simple log message:

~ Example () {
std :: cout << "Deleting Example, freeing SomeResource! \ n";
}

This means that anyone could (and even should) remove the destructor from the class, and instead rely on the version generated by the compiler. One function from the Big Three suddenly left without work for the class Example! However, we have to consider after such reflection that our simple example just worked with raw pointers; in real programs there are much more resources than raw pointers. And although many of them provide ways to get rid of them when they are deleted (again, working through RAII), others do not care about that. All this can be achieved without a destructor, and this will be the topic of the next section.

Note: diligent readers may notice that the RAII class described above is not the only implementation of RAII, and it should not be the only one, because the Standard C ++ library already has a similar implementation called std :: auto_ptr. It mainly works on the same principle as our RAII class is higher, only better. Why write your version? Because auto_ptr defines a public copy constructor and a copy assignment operator, both of which imply ownership of the resource, while we may need to make it copyable (the RAII class also does not care about it, but at least it reminds us that it is need to do). We need to copy the resource, and not pass quietly over the reins of its management, therefore, for the sake of simplicity of this example, it was enough for us to invent the wheel described above :)

Smart Pointers or Smart Resources?

Everything that we have shown in this article in terms of resource management exists in the implementation of each class of smart pointers (hundreds of programmers believe that the implementation of smart pointers in Boost is the best among them). But, as already mentioned, resource management is not only a call to delete, and it may require some special management logic, or just other ways to free resources (for example, call close ()). This is the reason that more and more smart pointers are becoming smart resources; in addition to supporting the automatic deletion of dynamically allocated resources, they allow various possibilities for calling user-defined methods for freeing occupied memory, or even the possibility of declaring a memory inline (inline) (this was possible using bind expressions and lambda expressions, for example, from Boost Lambda). Most of the code that was previously placed in the destructor of the aggregation class is now more closely combined with the resources (or with the resource holder), which ideally implement this operation. It will be interesting to look at further bugs in this area. Given the multithreading and the exception-safety of management, which goes far beyond what many of us have previously addressed (at least so for the authors), smart ways of managing resources are becoming more and more important.

Conclusion

The Big Three Law has played and continues to play its important role in C ++. However, we think that there is a reason to remove the destructor from the discussion as well as from the implementation, when it is possible, which leads us to the derived “Law of the Big Two”. The reason is that it is often necessary to avoid raw pointers in members of the class, and they should be replaced with smart pointers. Otherwise, the role of copy constructors and copy assignment operators is often forgotten and ignored; we hope this article will help point this out in some cases.

Acknowledgments

We would like to thank Marshall Cline for inventing the catchy “Big Three” in the C ++ FAQ. This helped many programmers remember to add copy constructors and copy assignment operators as needed.
Thanks to Bjarne Stroustrup for a brief explanation of RAII in the C ++ Style And Technique FAQ and in its immortal volume The C ++ Programming Language (3rd Edition).
Thanks to Chuck Allison for editing this article (and many of our other articles) and for her (their) improvements.
Thanks to Andrei Alexandrescu (Andrei Alexandrescu), David Abrahams (David Abrahams) and Torsten Ottosenu (Thorsten Ottose) for viewing the article.
Thanks to Andrei Alexandrescu, Bjarne Stroustrup, Chuch Allison, David Abrahams, Nathan Myers, and Walter Bright for discussing the ins and outs in function - try-blocks [13].
Thanks to Daniel Teske, Mehul Mahimtura, and Vesa Karvonen for significant notes and comments.

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


All Articles