RAII and unhandled exceptions

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

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

A typical (though not the only) way of implementation is to organize access to the resource in the constructor, and release in the destructor of the corresponding class. Since the destructor of an automatic variable is called when it leaves the scope, the resource is guaranteed to be freed when the variable is destroyed. This is also true in situations in which exceptions arise. This makes RAII a key concept for writing exception-safe code in programming languages, where the constructors and destructors of automatic objects are called automatically, primarily in C ++.

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

Example code using RAII:

Let's say there is some class encapsulating 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 fine, as RAII promises, even if an exception is thrown, the m_net pointer in the LockNet class will be correctly deleted. Right?

Unfortunately no.

For some reason, in the description of RAII, they usually forget to write that for this technique to work, the exception MUST be caught by an exception handler of this type, otherwise, if the handler is not found, std :: terminate () will be called, which will crash the program. Straustrup describes this in his book The C ++ Programming Language (03), chapter 14.7.

Removing local objects depends on the implementation, somewhere they will be deleted, somewhere vice versa, 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 it recommends that if you need guaranteed removal of local objects, wrap the code in the main function with a try - catch (...) block that catches any exceptions.

T.ch. in the main function code, if there is an exception before the return 0; operator, we get a normal resource leak.
It is not fatal, since the OS will save the coredump and release the resources occupied by the program.

How to verify this? Writing a verification code!

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

#include 
#include 
#include 
#include 
#include 
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 & m_share)
{
    m_share->sayName();
}
int main()
{
    vector> vec {make_shared("0"), make_shared("1"), make_shared("2"), make_shared("3"), make_shared("4")};
    for (auto& x:vec)
            testShared(x);
    return 0;
}

Having compiled and running this program, we get the conclusion:

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 function call that throws an exception in a try - catch block:

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

И, вуаля — всё начинает работать как и должно.

Вызываются не только конструкторы, но и деструкторы объектов, хранящихся в умных указателях.

Вывод программы:

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


Тем не менее, всё согласно стандарту:

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 in the reverse order of the
completion of their construction.

3. The process of calling destructors for automatic objects constructed on the path from a try 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
unwound before this call to std::terminate() is implementation-defined (15.5.1).


Проверялось на g++ (Ubuntu 4.9.2-0ubuntu1~14.04) 4.9.2, Visual Studio 2013 CE.

Ссылки


RAII
ISO C++

Also popular now: