[Из песочницы] 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») //здесь какие-то другие функции, //которые могут генерироваnь исключения return 0; } Вроде бы всё нормально, как обещает RAII, даже если будет сгенерировано исключение, указатель m_net в классе LockNet будет корректно удалён. Правильно? Увы, нет.
Почему-то в описании RAII обычно забывают написать, что для работы этой техники исключение ОБЯЗАНО быть перехвачено обработчиком исключений этого типа, иначе, если обработчик не будет найден, будет вызвана std: terminate (), которая аварийно завершит выполнение программы. Страуструп описывает это в книге «Язык программирования С++ (03)», глава 14.7.
Удаление локальных объектов зависит от реализации, где-то они будут удалены, где-то наоборот, чтобы разработчик мог увидеть состояние локальных объектов на момент исключения, в дебагере когда загрузит coredump. И рекомендует если вам нужно гарантированное удаление локальных объектов оборачивать код в функции main блоком try — catch (…), который перехватывает любые исключения.
Т.ч. в коде функции main, если будет исключение до оператора return 0;, мы получаем обычную утечку ресурсов.Она не фатальная, так как ОС сохранит coredump и освободит ресурсы, занятые программой.
Как в этом убедится? Пишем проверочный код!
В данном коде мы используем умные указатели, которые реализовывают технику RAII:
#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
int main ()
{
vector
for (auto& x: vec) testShared (x);
return 0; } Скомпилировав и запустив эту программу, получаем вывод: Constructor of slot: 0terminate called after throwing an instance of 'MyExc’Constructor of slot: 1Constructor of slot: 2Constructor of slot: 3Constructor of slot: 4Slot name is: 0
Переписываем функцию testShared, заворачивая вызов функции генерирующей исключение в пустой try — catch блок:
void testShared (shared_ptr
} } И, вуаля — всё начинает работать как и должно.Вызываются не только конструкторы, но и деструкторы объектов, хранящихся в умных указателях.
Вывод программы:
Constructor of slot: 0Constructor of slot: 1Constructor of slot: 2Constructor of slot: 3Constructor of slot: 4Slot name is: 0Slot name is: 1Slot name is: 2Slot name is: 3Slot name is: 4Destructor of slot: 0Destructor of slot: 1Destructor of slot: 2Destructor of slot: 3Destructor 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 objectsconstructed since the try block was entered. The automatic objects are destroyed in the reverse order of thecompletion of their construction.
3. The process of calling destructors for automatic objects constructed on the path from a try block to athrow-expression is called «stack unwinding.» If a destructor called during stack unwinding exits with anexception, 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 isunwound 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.
Ссылки RAIIISO C++