
Good afternoon,
Serega again got to the keyboard and talks about C ++. Today we will talk about what else classes need in C ++, how destructors work and what other rake can be stepped on if you mix two languages. Under the cut there is nothing new and outstanding for those who know C ++ since the days of DOS. If you are just learning this language - welcome.
As you probably noticed last time, a class has a very important thing - a destructor! This function will be called ALWAYS when the class is destroyed. No matter what happened, exit from a function in the middle or even a thrown exception, the class destructor will be called anyway while the program is running. (For reference, the program no longer works if no one catches an exception, so you need to put try ... catch on all types of exceptions in main.). On C, the destructor is called manually, which forces you to write a lot of unnecessary details.
if(!Create1(...)) return -1; if(!Create2(...)) { Destroy1(...); return -1; } … if(!CreateN(...)) { Destroy1(...); Destroy2(...); ... DestroyN-1(...); return -1; }
And additional branches and loops only add confusion and bulkiness in the code.
The destructor is not replaced by the finalize section available in other languages! The fact is that it is called only for already created objects, and it is not always possible to figure out who has already been created and who is not in finalize. It is necessary to fence nested blocks and multiple sections of finalization, which also makes the code unnecessarily confusing. Here is a good example of such a situation:
#include <iostream> #include <stdexcept> class Foo { private: static int sm_count; int instance; public: Foo() { instance = ++sm_count; if(instance > 5) throw std::runtime_error(" 5 ."); std::cout << " â„– " << instance << " Foo .\n"; } ~Foo() { std::cout << " â„– " << instance << " Foo .\n"; } }; int Foo::sm_count = 0; int main() { try { Foo* pFoo = new Foo[10]; } catch(const std::exception& e) { std::cout << e.what() << "\n"; } return 0; }
Competent and ubiquitous use of destructors brings C ++ closer in style to programming languages ​​with the garbage collector. With this approach, the programmer is busy building a model, rather than tracking down symmetric selections and freeing up resources. However, such a rapprochement is highly deceptive and often ends with an appeal to an object removed from the heap and an emergency exit from the program. And if you still actively use the constructions of the C language (referred to last time as “C with classes”), then once guaranteed to access the data of an object of one type through a pointer to another type. Here is a very good example (it is not necessary to divide into header files, but it is useful to make it easy to change the order of their inclusion):
In this example, try changing the order in which the second and third header files are included. It is very good when such errors are easy to detect, as in this isolated example. When code is scattered across many files, hidden by several levels of inheritance hierarchy and virtual operator overload, it’s easier to go crazy than to understand what actually happens. If pure C ++ is used in this example, the compiler will generate an error at compile time.
Let's return to destructors. When writing them, the main thing is not to accidentally throw an exception. The fact is that a destructor can be called exactly in the course of exception handling. In this case, the process of unwinding the stack of the procedure of unwinding the stack ends in the most simple and expected way: immediate exit from the program. So watch them carefully. In no case do not allocate them memory, resources, and especially do not throw exceptions explicitly. This requirement dictates a certain attitude towards all “closing” functions. For example, some
Socket::Close()
can no longer make
throw std::runtime_error("Close socket error: socket was not opened")
. The fact is that the most logical thing to do with the “closing” function is to call it in the destructor. But it’s not even logical to call this call with various condition checks. And if you work in a team, then someone will definitely try to close the already closed. So, write down a simple rule for yourself: in any “closing” function, you need to quietly and without unnecessary movements to do exactly what is required of it - to “close” what is asked, even if it is physically impossible.
Once again I draw attention to the fact that it is impossible to allocate or seize anything in such functions and destructors. Very common mistake:
~object::object() { g_logger.put_message(std::string("Object ") + m_name + std::string(" was deleted.")); }
If this destructor is called in the process of processing the exception "out of memory", then you will be looking for the cause of the program departure for a long time. However, even the logger mentioned here will not help, since will not be able to form and save the message so desired. How to be in this situation? The easiest way, but not the most “beautiful”, is to generate a message in advance, when everything was fine, and keep it in a private part for the time being. More "advanced" option:
~object::object() { g_logger << "Object " << m_name << " was deleted."; }
I hope it is clear that g_logger here has no right to deal with allocations of memory and file openings, but is it obliged to have a fixed-size buffer ready and merge it into a previously opened file for filling?
Smoothly proceed to the allocation of resources. The correct approach is to first select and then use. Here is a wrong example:
std::vector<int> items; ... items.push_back(item1); items.push_back(item2); : std::vector<int> items; ... items.reserve(items.size() + 2); items.push_back(item1); items.push_back(item2);
We must constantly remember that the memory may end at the most inopportune moment, and the state of the program at such a moment must remain certain. If there must be two elements in the array, it means either two integers or none. There should be no "under-created" data structures, since This is a direct way to crash when working destructors. A good program is one that even when there is a shortage of memory, saves its data correctly and goes out quietly. Well, maybe not quiet, but politely saying goodbye. Unfortunately, this is not always so easily achievable. For example,
std::list
does not have a reserve method. For such cases, you have to set up the "empty" state of the data element, such as null_ptr for pointers or -1 for indexes, and put it first in the data structure. And in the destructor carefully bypass such elements. It is appropriate to recall the fascination with all sorts of operators that create temporary objects on the stack. These objects, in turn, allocate resources that are not allocated, but throw an exception right in the middle of a complex expression, leaving parts of this expression in a semi-exhausted state. For example, an iterator shifted by the ++ operator in the middle of an expression will be absolutely useless in the catch section.
')
In order for the integrity of data structures to be obtained by itself, without unnecessary gestures on the part of the programmer, it is necessary to strive to ensure that all such structures are created in constructors and represent an object, but destroyed by destructors, correctly excluding themselves from the overall data structure of the program. The above example with integers should be written like this:
typedef std::pair<int, int> DataItem; std::vector<DataItem> items; ... items.push_back(DataItem(item1, item2));
However, this is no longer such a simple topic of modeling the subject area of ​​the task.