В дополнении к отличному ответу quantdev (который я upvoted), я хотел бы также продемонстрировать вопросы безопасности за исключение делегировали конструкторы для тех типов, которые должны явно приобрести несколько ресурсов в конструкторе и явно удалять несколько ресурсов в своем деструкторе.
В качестве примера я буду использовать простые исходные указатели. Обратите внимание, что этот пример не очень мотивирован, потому что использование интеллектуальных указателей над необработанными указателями решает проблему более аккуратно, чем делегирование конструкторов. Но пример прост. Все еще существуют более сложные примеры, которые не решаются с помощью интеллектуальных указателей.
Рассмотрит два класса X
и Y
, которые нормальные классы, за исключением того, что я украсил свои специальные элементы с заявлениями для печати, чтобы мы могли их видеть, и 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
), я настоятельно рекомендую эту идиому для управления безопасностью исключения.
Update
Возможно, более мотивация Например, пользовательский контейнерный класс (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);
}
Если кто-то в обзоре кода назвал последнюю плохую практику, я бы пошел на коврик на этом.
Это уменьшает дублирование кода, что само по себе можно считать хорошим. Некоторые инициализации не могут быть выполнены в функциональном теле конструктора, поэтому они не могут быть помещены в функцию-член. Чтобы не повторять их, вам нужно либо использовать базовый класс, либо конструктор делегирования (или инициализаторы нестатических элементов данных, но они не могут использовать параметры конструктора и т. Д.). – dyp
Когда у вас есть несколько конструкторов, вы обычно обнаруживаете, что почти весь код среди них одинаковый.Альтернативой является создание функций-членов инициализации. Я хотел бы знать, как самая бесполезная функция на C++ когда-либо попадала в спецификации: throw для функций. – user3344003
Если у вас есть 10 членов для инициализации и 4 конструктора, это становится болью. Даже добавление, удаление изменения члена означает посещение всех ваших конструкторов и отражение изменений там. Лучше иметь возможность сделать это в одном месте. – Galik