📜 ⬆️ ⬇️

RAII and unhandled exceptions

Surely everyone knows the capital (in the books about C ++) the truth about the wonderful methodology of RAII, if not - I will give a brief description from Wikipedia.

This is a sample description of this technique.
Resource Acquisition Is Initialization (RAII) is a software idiom of object-oriented programming, the meaning of which is that using various software mechanisms, obtaining a certain resource is inextricably combined with initialization, and releasing it with destruction object.

A typical (though not the only) way to implement is to organize access to the resource in the constructor, and the release in the destructor of the corresponding class. Since the destructor of an automatic variable is called when it goes out of scope, the resource is guaranteed to be freed when the variable is destroyed. This is true in situations where exceptions occur. This makes RAII a key concept for writing safe with code exceptions in programming languages, where constructors and destructors of automatic objects are called automatically, first of all in C ++.

The last sentence seems to promise a 100% guaranteed result, but as always in life, and especially in C ++, there is a nuance.

Sample code using RAII:
')
Suppose there is some class that encapsulates network access:

class Network{ public: Network(const URL &url) : m_url(url) { } private: Url m_url; }; 

Create a class that will implement RAII:

 class LockNet{ public: LockNet(const Url &url){ m_net = new Network(url); } ~LockNet (){ delete m_net; } operator Network * (){ return network; } private: Network *m_net; }; 

Now in the main function we can safely use this resource:

 int main(int argc, char *argv[]) { LockNet net("http://habrahabr.ru") // -  , //    return 0; } 

Everything seems to be normal, as RAII promises, even if an exception is generated, the m_net pointer in the LockNet class will be correctly deleted. Right?

Unfortunately no.

For some reason, in the description of RAII, it is usually forgotten to write that for the operation of this technique, an exception MUST be intercepted by an exception handler of this type, otherwise, if the handler is not found, std :: terminate () will be terminated, which will crash the program. Stroustrup describes this in the C ++ (03) Programming Language book, chapter 14.7.

Deleting local objects depends on the implementation, somewhere they will be deleted, somewhere on the contrary, so that the developer can see the state of local objects at the time of the exception in the debugger when it loads coredump. And recommends if you need guaranteed removal of local objects to wrap the code in the main function with a try block - catch (...), which catches any exceptions.

T.ch. in the code of the main function, if there is an exception before the operator return 0 ;, we get the usual leakage of resources.
It is not fatal, since the OS will save coredump and free up the resources occupied by the program.

How to make sure of it? Writing a verification code!

In this code, we use smart pointers that implement the RAII technique:

 #include <iostream> #include <vector> #include <memory> #include <string> #include <exception> using namespace std; class MyExc { }; class Slot { public: Slot(const std::string &str = "NONAME") : m_name(str) { cout << "Constructor of slot: " << m_name << endl; } virtual ~Slot() { cout << "Destructor of slot: " << m_name << endl; } void sayName() { cout << "Slot name is: " << m_name << endl; throw MyExc(); } private: string m_name; }; void testShared(shared_ptr<Slot> & m_share) { m_share->sayName(); } int main() { vector<shared_ptr<Slot>> vec {make_shared<Slot>("0"), make_shared<Slot>("1"), make_shared<Slot>("2"), make_shared<Slot>("3"), make_shared<Slot>("4")}; for (auto& x:vec) testShared(x); return 0; } 

Compiling and running this program, we get the output:

Constructor of slot: 0
terminate called after throwing an instance of 'MyExc'
Constructor of slot: 1
Constructor of slot: 2
Constructor of slot: 3
Constructor of slot: 4
Slot name is: 0

We rewrite the main function, wrapping the call to the function that generates an exception in the try - catch block:

 int main() { try { vector<shared_ptr<Slot>> vec{make_shared<Slot>("0"), make_shared<Slot>("1"), make_shared<Slot>("2"), make_shared<Slot>("3"), make_shared<Slot>("4")}; for (auto &x:vec) testShared(x); } catch (std::exception & ex){ cout<<ex.what()<<endl; } catch (...){ cout<<"Unexpected exception"<<endl; } 

And, voila - everything starts working as it should.

Not only constructors are called, but also destructors of objects stored in smart pointers.

The output of the program:

Constructor of slot: 0
Constructor of slot: 1
Constructor of slot: 2
Constructor of slot: 3
Constructor of slot: 4
Slot name is: 0
Unexpected exception
Destructor of slot: 0
Destructor of slot: 1
Destructor of slot: 2
Destructor of slot: 3
Destructor of slot: 4


However, everything is according to the standard:

15.2 Constructors and destructors

1. As control passes from a throw-expression to a handler, destructors are invoked for all automatic objects.
constructed since the try block was entered. The automatic objects are destroyed.
completion of their construction.

3. Try the block to a
throw-expression is called “stack unwinding.” If a destructor called during stack unwinding exits with an
exception, std :: terminate is called (15.5.1).

15.3 Handling an exception

9 If no matching handler is found, the function std :: terminate () is called; whether or not the stack is
call this std :: terminate () is implementation-defined (15.5.1).

It was tested on g ++ (Ubuntu 4.9.2-0ubuntu1 ~ 14.04) 4.9.2, Visual Studio 2013 CE.

Links


RAII
ISO C ++

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


All Articles