В этом посте пойдет речь об одной интересной фичи в 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 << "dtorn";
}
};
int main()
try
{
MyClass obj(10);
cout << "obj created";
}
catch (...)
{
cout << "exceptionn";
}
Конструктор 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.
Автор: alexeiz