Кратко об RAII (ну очень кратко)
Когда нам нужно автоматизировать управление каким-нибудь “голым” ресурсом, мы его “заворачиваем” в отдельный класс. Продемонстрируем это на примере такого ресурса как FILE из стандартной библиотеки C:
#include <stdio.h> class File { public: File(char const * filename, char const * mode) : file_(fopen(filename, mode)) {} ~File() { fclose(file_); } File(File const &) = delete; File operator=(File const &) = delete; // file operations // ... private: FILE * file_; };
Здесь мы создаем FILE ресурс в конструкторе и освобождаем в деструкторе. Теперь ресурс FILE управляется в полном соответствии с идиомой RAII.
Немного более усложненный случай RAII
Допустим теперь, что в дополнение к открытию файла в конструкторе нам нужно провести с ним некоторую операцию. Например, будем в заново открытый файл записывать время последнего открытия, time stamp. Для этого создадим в классе File функцию put_time_stamp, которая в каким-то образом помещает в файл time stamp, а в случае неудачи выбрасывает какое-то исключение.
Реализуется это дело как-то так:
#include <stdio.h> class File { public: File(char const * filename, char const * mode) : file_(fopen(filename, mode)) { put_time_stamp(); } ~File() { fclose(file_); } File(File const &) = delete; File operator=(File const &) = delete; // file operations void put_time_stamp() { // throws on error // ... } private: FILE * file_; };
Но как видно, в данной реализации есть небольшая проблема. Конструктор File перестал быть exception safe. Если из put_time_stamp вылетит исключение, то оно не приведет в вызову деструктора объекта File, так как его конструктор еще не завершился. Поэтому ресурс file_ будет потерян.
Как нам решить эту проблему? Тупое решение “в лоб” заключается в оборачивании вызова put_time_stamp в блок try/catch:
class File { public: File(char const * filename, char const * mode) try : file_(fopen(filename, mode)) { put_time_stamp(); } catch (...) { destruct_obj(); } ~File() { destruct_obj(); } private: void destruct_obj() { fclose(file_); } FILE * file_; };
Этот подход работает, но он немного некрасив из-за необходимости иметь явный try/catch блок и отдельный метод для явного разрушения объекта, чтобы не дублировать одну и ту же функциональность в catch блоке и в деструкторе.
Мы можем немного улучшить данное решение, если введем дополнительный класс специально для хранения и удаления FILE, FileHandle:
class File { struct FileHandle { FileHandle(FILE * fh) : fh_(fh) {} ~FileHandle() { fclose(fh_); } FILE * fh_; } public: File(char const * filename, char const * mode) : file_(fopen(filename, mode)) { put_time_stamp(); } ~File() = default; private: FileHandle file_; };
Как видно, теперь явный try/catch блок уже не нужен. Объект file_ будет корректно разрушен, даже если из конструктора класса File вылетит исключение, и ресурс FILE будет освобожден. Но в этом решении все равно есть некоторый недостаток, заключающийся в отдельном классе FileHandle, который разносит создание и освобождение ресурса FILE на два разных класса: FILE создается в классе File, а освобождается в классе FileHandle.
Делегирующие конструкторы
Рассмотрим теперь одну очень полезную фичу из C++11 под названием делегирующие конструкторы, которая позволит нам еще более улучшить предыдущий код класса File. Но для начала, посмотрим, как вообще работают эти делегирующие конструкторы.
Допустим, у нас есть класс с двумя конструкторами: один от параметра типа int, а другой от double. Конструктор для int делает то же самое, что и конструктор для double, только сначала он переводит параметр от типа int к типу double. Т.е. конструктор для int делегирует создание объекта конструктору для double. Вот как это выглядит в коде:
class MyClass { public: MyClass(double param) { // construct object for double parameter } MyClass(int param) : MyClass(double(param)) // call ctor for double { // do some additional operations for int parameter // if necessary } };
После того, как конструктор для double закончит выполнение, конструктор для int может продолжить выполняться и “доконструировать” объект. Сама по себе это очень полезная фича, без которой в коде выше нам наверняка пришлось бы ввести дополнительную функцию init(double param) для энкапсуляции общего кода по созданию объект от типа double.
Но в дополнение у этой фичи есть один очень интересный побочный эффект. Дело в том, что как только один из конструкторов объекта закончит выполнение, объект считается созданным. И значит, если другой конструктор, из которого произошел делегирующий вызов первого конструктора, завершится с выбросом исключения, для этого объекта все равно будет вызван деструктор. Заметьте критический момент: для объекта теперь может выполниться больше одного конструктора. Но объект считается созданным после выполнения самого первого конструктора.
Продемонстрируем это поведение на следующем примере:
class MyClass { public: MyClass(double) { cout << "ctor(double)\n"; } MyClass(int val) : MyClass(double(val)) { cout << "ctor(int)\n"; throw "oops!"; } ~MyClass() { cout << "dtor\n"; } }; int main() try { MyClass obj(10); cout << "obj created"; } catch (...) { cout << "exception\n"; }
Конструктор MyClass(int) вызывает другой конструктор MyClass(double), после чего сам выбрасывает исключение. Это исключение ловится в catch(…), и при раскрутке стека вызывается деструктор ~MyClass. На консоль при выполнении данного кода выведется следующее:
ctor(double) ctor(int) dtor exception
Делегирующие конструкторы и RAII
Нетрудно заметить, что такое интересное поведение конструкторов при делегировании можно очень эффективно использовать в нашем примере реализации RAII для FILE. Теперь нам не нужно вводить никакой дополнительный класс FileHandle для освобождения ресурса FILE, а тем более не нужен и try/catch. Нужно ввести всего лишь один дополнительный конструктор, которому будет произведена делегация из основного конструктора. То есть:
class File { File(FILE * file) : file_(file) {} public: File(char const * filename, char const * mode) : File(fopen(filename, mode)) { put_time_stamp(); } ~File() { fclose(file_); } void put_time_stamp() { ... } private: FILE * file_; };
И это все что нам необходимо. Очень красиво, элегантно и полностью безопасно по отношению к исключениям (exception safe). Вывод: подобная техника существенно облегчит реализацию идиомы RAII в новом коде с использованием делегирующих конструкторов из C++11.
ссылка на оригинал статьи http://habrahabr.ru/post/157315/
Добавить комментарий