📜 ⬆️ ⬇️

Safe constructors

A recent article on the order of initialization of class members caused a very interesting discussion, in which, among others, the question of how to properly classify members of a class, whether to store them by value and organize a constructor like this was discussed:

  A :: A (int x): b (x) {} 

Or store them by reference:

  A :: A (int x) {b = new B (x);  } 

There are many pros and cons for each approach, but in this article I would like to focus on the issues of exception handling.
')
Let's start in order. Suppose we have a certain class, the constructor of which, in some cases, can cause an exception (no file, no connection, password did not fit, not enough rights to perform the operation ... anything). Our class will be extremely simple and predictable.

  class X {
 private:
   int xx;
 public:
   X (int x) {
     cout << "X :: X x =" << x << endl;
     if (x == 0) throw (exception ());
     xx = x;
   }
   ~ X () {
     cout << "X :: ~ X x =" << xx << endl;
   }
 }; 

That is, if the constructor argument is zero, an exception is triggered.

Suppose we need a class, whose objects must contain two objects of class X.

Option one - with pointers (caution, dangerous code!)


We do everything in science:

  class Cnt {
 private:
   X * xa;
   X * xb;
 public:
   Cnt (int a, int b) {
     cout << "Cnt :: Cnt" << endl;
     xa = new X (a);
     xb = new X (b);
   }
   ~ Cnt () {
     cout << "Cnt :: ~ Cnt" << endl;
     delete xa;
     delete xb;
   }
 }; 

It would seem nothing is forgotten. (Although, strictly speaking, of course, we forgot at least the copy constructor and the assignment operation, which would work correctly with our pointers; but oh well.)

Let's use this class:

  try {
   Cnt c (1, 0);
 } catch (...) {
   cout << "error" << endl;
 } 

And let's figure out what and when will be constructed and destroyed.


Everything. The constructor will stop its work, and the destructor of the Cnt object will not be called (and this is correct, the object was not created). Total, what do we have? One object X, the pointer to which (xa) is lost forever. In this place, we immediately get a memory leak, and possibly get a leak and more valuable resources, soktov, cursors ...

Please note that this is one of the most unpleasant situations, a leak does not always occur, but only with certain arguments (the first is not zero, and the second is zero). Finding such leaks can be very difficult.

Obviously, such a solution is suitable only for very simple little programs, which in the case of any exception just fall helplessly and that's it.

What are the solutions?

The simplest, most reliable and natural solution is to store an object by value.


Example:

  class Cnt {
 private:
   X xa;
   X xb;
 public:
   Cnt (int a, int b): xa (a), xb (b) {
     cout << "Cnt :: Cnt" << endl;
   }
   ~ Cnt () {
     cout << "Cnt :: ~ Cnt" << endl;
   }
 }; 

It is compact, it is elegant, it is natural ... but the main thing is safe! In this case, the compiler keeps track of everything that happens, and (if possible) cleans everything that is no longer needed.

The result of the code:

  try {
   Cnt c (1, 0);
 } catch (...) {
   cout << "error" << endl;
 } 

will be like this:

  X :: X x = 1
 X :: X x = 0
 X :: ~ X x = 1
 error 

That is, the Cnt :: xa object was automatically correctly deleted.

Crazy decision with pointers


The following solution can be a real nightmare:

  Cnt (int a, int b) {
   cout << "Cnt :: Cnt" << endl;
   xa = new X (a);
   try {
     xb = new X (b);
   } catch (...) {
     delete xa;
     throw;
   }
 } 

Can you imagine what will happen if Cnt :: xc appears? And if you have to change the order of initialization? .. It will be necessary to make a lot of effort not to forget anything, accompanying such code. And, what is the most offensive, it was you who laid the rake everywhere for yourself.

Lyrical digression about exceptions.


What were the exceptions invented for? In order to separate the description of the normal course of the program from the description of the reaction to any failures.

In this example, we grossly trample on this beautiful doctrine. We have to place the code that handles the exception in close proximity to the code that causes the exception.

This negates the charm of the exception mechanism. In fact, we return to the concept of C, where after each operation it is necessary to check the values ​​of global variables or other signs of errors.

This makes the code confusing and difficult to understand and maintain.

The solution of these Indians - smart pointers


If you still need to store pointers, then you can still secure your code if you make wrappers for pointers. You can write them yourself, and you can use a lot of existing ones. An example of using auto_ptr:

  class Cnt {
 private:
   auto_ptr <x> ia;
   auto_ptr <X> ib;
 public:
   Cnt (int a, int b): ia (new X (a)), ib (new X (b)) {
     cout << "Cnt :: Cnt" << endl;
   }
   ~ Cnt () {
     cout << "Cnt :: ~ Cnt" << endl;
   }
 }; 

We practically returned to the solution with the storage of class members by value. Here we store the objects of the auto_ptr <X> class by value, the compiler takes care of the timely removal of these objects (note, now we don’t have to call delete on the destructor ourselves); and they, in turn, store our pointers to objects X and make sure that the memory is freed in time.

Yes! And do not forget to connect

  #include <memory> 

It describes the auto_ptr template.

Lyrical digression about new


One of the advantages of C ++ over C is that C ++ allows you to work with complex data structures (objects) as with ordinary variables. That is, C ++ itself creates these structures and deletes them itself. A programmer may not think about freeing resources until he (the programmer) starts creating objects himself. As soon as you wrote "new", you committed yourself to write "delete" wherever it is needed. And this is not only destructors. Moreover, you will most likely have to independently implement the copy operation and the assignment operation ... In a word, you refused the services of C ++ and got into a very fragile ground.

Of course, in real life you often have to use “new”. This may be due to the specifics of the algorithm, dictated by performance requirements, or simply imposed on other people's interfaces. But if you have a choice, then you should probably think three times, before writing the word "new".

Successes to all! And may your memory never flow!

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


All Articles