RAII и делегирующие конструкторы в C++11

от автора

В этом посте пойдет речь об одной интересной фичи в C++11, которая называется делегирующие конструкторы (delegating constructors): почему она интересна, и как ее можно применить для более эффективного управления ресурсами, т.е. реализации идиомы RAII.

Кратко об 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *