Почему в C++11 появились делегирующие конструкторы?
Я не могу понять, что толку от делегирования конструкторов. Проще говоря, чего нельзя достичь без делегирования конструкторов?
Это может сделать что-то простое, как это
class M
{
int x, y;
char *p;
public:
M(int v) : x(v), y(0), p(new char [MAX]) {}
M(): M(0) {cout<<"delegating ctor"<<endl;}
};
Но я не вижу, стоит ли вводить новую функцию для такой простой вещи? Может быть, я не мог распознать важный момент. Любая идея?
5 ответов
Делегирующие конструкторы предотвращают дублирование кода (а также все возможные ошибки и недостатки, которые сопровождают его: повышенное обслуживание, снижение читабельности...), и это хорошо.
Это также единственный способ делегировать список инициализации (для инициализаций членов и баз), т.е. вы действительно не можете заменить эту функцию, имея общий доступ Init()
метод для ваших конструкторов.
Примеры:
1) Общая инициализация из предложения N1986:
class X {
X( int, W& );
Y y_;
Z z_;
public:
X();
X( int );
X( W& );
};
X::X( int i, W& e ) : y_(i), z_(e) { /*Common Init*/ }
X::X() : X( 42, 3.14 ) { SomePostInitialization(); }
X::X( int i ) : X( i, 3.14 ) { OtherPostInitialization(); }
X::X( W& w ) : X( 53, w ) { /* no post-init */ }
2) Делегирование с конструктором и конструктором копирования, также из предложения N1986:
class FullName {
string firstName_;
string middleName_;
string lastName_;
public:
FullName(string firstName, string middleName, string lastName);
FullName(string firstName, string lastName);
FullName(const FullName& name);
};
FullName::FullName(string firstName, string middleName, string lastName)
: firstName_(firstName), middleName_(middleName), lastName_(lastName)
{
// ...
}
// delegating copy constructor
FullName::FullName(const FullName& name)
: FullName(name.firstName_, name.middleName_, name.lastName_)
{
// ...
}
// delegating constructor
FullName::FullName(string firstName, string lastName)
: FullName(firstName, "", lastName)
{
// ...
}
3) MSDN приводит этот пример с конструкторами, выполняющими проверку аргументов (как прокомментировано, этот дизайн является дискуссионным):
class class_c {
public:
int max;
int min;
int middle;
class_c() {}
class_c(int my_max) {
max = my_max > 0 ? my_max : 10;
}
class_c(int my_max, int my_min) {
max = my_max > 0 ? my_max : 10;
min = my_min > 0 && my_min < max ? my_min : 1;
}
class_c(int my_max, int my_min, int my_middle) {
max = my_max > 0 ? my_max : 10;
min = my_min > 0 && my_min < max ? my_min : 1;
middle = my_middle < max && my_middle > min ? my_middle : 5;
}
};
Благодаря делегированию конструкторов это сокращает до:
class class_c {
public:
int max;
int min;
int middle;
class_c(int my_max) {
max = my_max > 0 ? my_max : 10;
}
class_c(int my_max, int my_min) : class_c(my_max) {
min = my_min > 0 && my_min < max ? my_min : 1;
}
class_c(int my_max, int my_min, int my_middle) : class_c (my_max, my_min){
middle = my_middle < max && my_middle > min ? my_middle : 5;
}
};
Ссылки:
В дополнение к отличному ответу Quantdev (за который я проголосовал), я также хотел продемонстрировать проблемы безопасности исключений при делегировании конструкторов тем типам, которые должны явно получать несколько ресурсов в конструкторе, и явно распоряжаться несколькими ресурсами в его деструкторе.
В качестве примера я буду использовать простые сырые указатели. Обратите внимание, что этот пример не очень мотивирует, потому что использование умных указателей над необработанными указателями решит проблему более аккуратно, чем делегирование конструкторов. Но пример прост. Все еще существуют более сложные примеры, которые не решаются умными указателями.
Рассмотрим два класса X
а также Y
, которые являются обычными классами, за исключением того, что я украсил их специальные члены инструкциями print, чтобы мы могли их видеть, и Y
имеет конструктор копирования, который может генерировать (в нашем простом примере он всегда генерируется только для демонстрационных целей):
#include <iostream>
class X
{
public:
X()
{
std::cout << "X()\n";
}
~X()
{
std::cout << "~X()\n";
}
X(const X&)
{
std::cout << "X(const&)\n";
}
X& operator=(const X&) = delete;
};
class Y
{
public:
Y()
{
std::cout << "Y()\n";
}
~Y()
{
std::cout << "~Y()\n";
}
Y(const Y&)
{
throw 1;
}
Y& operator=(const Y&) = delete;
};
Теперь демонстрационный класс Z
который содержит управляемый вручную указатель на X
и Y
просто для создания "нескольких вручную управляемых ресурсов".
class Z
{
X* x_ptr;
Y* y_ptr;
public:
Z()
: x_ptr(nullptr)
, y_ptr(nullptr)
{}
~Z()
{
delete x_ptr;
delete y_ptr;
}
Z(const X& x, const Y& y)
: x_ptr(new X(x))
, y_ptr(new Y(y))
{}
};
Z(const X& x, const Y& y)
Конструктор в его нынешнем виде не является безопасным для исключения. Показывать:
int
main()
{
try
{
Z z{X{}, Y{}};
}
catch (...)
{
}
}
какие выводы:
X()
Y()
X(const&)
~Y()
~X()
X
был построен дважды, но разрушен только один раз. Есть утечка памяти. Есть несколько способов сделать этот конструктор безопасным, один из них:
Z(const X& x, const Y& y)
: x_ptr(new X(x))
, y_ptr(nullptr)
{
try
{
y_ptr = new Y(y);
}
catch (...)
{
delete x_ptr;
throw;
}
}
Пример программы теперь правильно выводит:
X()
Y()
X(const&)
~X()
~Y()
~X()
Однако вы можете легко увидеть это, когда вы добавляете управляемые ресурсы в Z
, это быстро становится громоздким. Эта проблема решается очень элегантно путем делегирования конструкторов:
Z(const X& x, const Y& y)
: Z()
{
x_ptr = new X(x);
y_ptr = new Y(y);
}
Этот конструктор сначала делегирует конструктору по умолчанию, который ничего не делает, кроме как переводит класс в допустимое состояние без ресурсов. Как только конструктор по умолчанию завершится, Z
в настоящее время считается полностью построенным. Так что если что-нибудь в теле этого конструктора выбрасывает, ~Z()
теперь работает (в отличие от предыдущих примеров реализации Z(const X& x, const Y& y)
, А также ~Z()
правильно очищает ресурсы, которые уже были созданы (и игнорирует те, которые не были созданы).
Если вам нужно написать класс, который управляет несколькими ресурсами в его деструкторе, и по каким-либо причинам вы не можете использовать другие объекты для управления этими ресурсами (например, unique_ptr
), Я настоятельно рекомендую эту идиому для управления безопасностью исключений.
Обновить
Возможно, более мотивирующим примером является пользовательский контейнерный класс (std::lib не предоставляет все контейнеры).
Ваш контейнерный класс может выглядеть так:
template <class T>
class my_container
{
// ...
public:
~my_container() {clear();}
my_container(); // create empty (resource-less) state
template <class Iterator> my_container(Iterator first, Iterator last);
// ...
};
Один из способов реализации конструктора-шаблона:
template <class T>
template <class Iterator>
my_container<T>::my_container(Iterator first, Iterator last)
{
// create empty (resource-less) state
// ...
try
{
for (; first != last; ++first)
insert(*first);
}
catch (...)
{
clear();
throw;
}
}
Но вот как я бы это сделал:
template <class T>
template <class Iterator>
my_container<T>::my_container(Iterator first, Iterator last)
: my_container() // create empty (resource-less) state
{
for (; first != last; ++first)
insert(*first);
}
Если бы кто-то в обзоре кода назвал последнюю плохую практику, я бы пошел на мат на этом.
Одним из ключевых применений делегирующих конструкторов, который не просто уменьшает дублирование кода, является получение дополнительных пакетов параметров шаблона, в частности последовательности целочисленных индексов, необходимых для указания инициализатора элемента:
Например:
struct constant_t;
template <class T, size_t N>
struct Array {
T data[N];
template <size_t... Is>
constexpr Array(constant_t, T const &value, std::index_sequence<Is...>)
: data { (Is,value)... }
{}
constexpr Array(constant_t, T const &value)
: Array(constant_t{}, value, std::make_index_sequence<N>{})
{}
};
Таким образом, мы можем определить конструктор, который инициализирует массив постоянным значением без предварительной инициализации каждого элемента по умолчанию. Насколько мне известно, единственный другой способ добиться этого - это поместить элемент данных в базовый класс.
Конечно, лучшая языковая поддержка пакетов параметров шаблона может сделать это ненужным.
Я описал другое использование делегирования конструкторов в Перегрузке #113, которое упрощает решения, описанные в Сложной Логике Кассио Нери в Списке инициализаторов членов в Перегрузке #112.
В отличие от кода внутри тела функции, когда вы пишете инициализаторы члена конструктора, вы не можете создать локальную переменную для хранения промежуточного результата, который необходим более чем одному из членов.
Рассмотрим конструктор так:
double some_expensive_calculation(double d);
bar::bar(double d)
: x_(cos(some_expensive_calculation(d))), y_(sin(some_expensive_calculation(d)))
{ }
Мы хотим не выполнять дорогостоящий расчет дважды (и в контексте исходной проблемы, описанной Кассио, базовый класс также хочет получить результат вычисления, поэтому вы не можете просто назначить x_
а также y_
в теле конструктора).
Уловка, которую я описал, состоит в том, чтобы вычислить промежуточный результат и делегировать другому конструктору, который использует этот результат:
class bar {
struct tag { };
...
bar(double result, tag);
public:
bar(double d);
};
bar::bar(double d)
: bar(some_expensive_calculation(d), tag{})
{ }
bar::bar(double result, tag)
: x_(cos(result)), y_(sin(result))
{ }
Мне кажется, что стоит упомянуть, что иногда предлагалось, чтобы дублирование кода между несколькими конструкторами можно было облегчить путем рефакторинга общего кода в частную функцию init. Проблема заключается в том, что если у класса есть друзья, эти друзья могут вызывать init несколько раз, и он не должен вызываться несколько раз. Делегирующие конструкторы предотвращают такие проблемы благодаря тому, что конструкторы не могут выполняться после того, как объект уже инициализирован.