понедельник, 6 февраля 2017 г.

Перенесу мой пост на хабре.

RAII и необрабатываемые исключения 

Наверняка все знают прописную (в книгах про С++) истину о чудесной методологии RAII, если нет — приведу краткое описание из википедии.

Это шаблонное описание этой техники
Получение ресурса есть инициализация (англ. Resource Acquisition Is Initialization (RAII)) — программная идиома объектно-ориентированного программирования, смысл которой заключается в том, что с помощью тех или иных программных механизмов получение некоторого ресурса неразрывно совмещается с инициализацией, а освобождение — с уничтожением объекта.

Типичным (хотя и не единственным) способом реализации является организация получения доступа к ресурсу в конструкторе, а освобождения — в деструкторе соответствующего класса. Поскольку деструктор автоматической переменной вызывается при выходе её из области видимости, то ресурс гарантированно освобождается при уничтожении переменной. Это справедливо и в ситуациях, в которых возникают исключения. Это делает RAII ключевой концепцией для написания безопасного при исключениях кода в языках программирования, где конструкторы и деструкторы автоматических объектов вызываются автоматически, прежде всего — в C++.

Последнее предложение вроде как обещает 100% гарантию результата, но как всегда в жизни, а особенно в С++, есть ньюанс. 

Пример кода использующего RAII:

Допустим, есть какой-то класс, инкапсулирующий доступ к сети:

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

private:
    Url m_url;

};

Создаём класс, который будет реализовывать 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;
};

Теперь в функции main мы можем безопасно использовать этот ресурс:

int main(int argc, char *argv[])
{
    LockNet net("http://habrahabr.ru")
    //здесь какие-то другие функции, 
    //которые могут генерировать исключения
    return 0; 
}

Вроде бы всё нормально, как обещает RAII, даже если будет сгенерировано исключение, указатель m_net в классе LockNet будет корректно удалён. Правильно?

Увы, нет.

Почему-то в описании RAII обычно забывают написать, что для работы этой техники исключение ОБЯЗАНО быть перехвачено обработчиком исключений этого типа, иначе, если обработчик не будет найден, будет вызвана std::terminate(), которая аварийно завершит выполнение программы. Страуструп описывает это в книге «Язык программирования С++ (03)», глава 14.7.

Удаление локальных объектов зависит от реализации, где-то они будут удалены, где-то наоборот, чтобы разработчик мог увидеть состояние локальных объектов на момент исключения, в дебагере когда загрузит coredump. И рекомендует если вам нужно гарантированное удаление локальных объектов оборачивать код в функции main блоком try — catch (...), который перехватывает любые исключения.

Т.ч. в коде функции main, если будет исключение до оператора return 0;, мы получаем обычную утечку ресурсов.
Она не фатальная, так как ОС сохранит coredump и освободит ресурсы, занятые программой.

Как в этом убедиться? Пишем проверочный код!

В данном коде мы используем умные указатели, которые реализовывают технику RAII:

#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<shared_ptr> vec {make_shared("0"), make_shared("1"), make_shared("2"), make_shared("3"), make_shared("4")};

    for (auto& x:vec)
            testShared(x);



    return 0;
}

Скомпилировав и запустив эту программу, получаем вывод:

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

Переписываем функцию main, заворачивая вызов функции генерирующей исключение в try — catch блок:

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<
    } catch (...){

        cout<<"Unexpected exception"<    }
}
  


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

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

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

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++