Ответ
Выброс исключения в конструкторе — стандартный и часто правильный способ сигнализировать об ошибке инициализации. Однако это создает специфические проблемы, которые нужно учитывать.
Основные проблемы:
- Частично сконструированный объект: Если исключение выброшено после выделения некоторых ресурсов (память, файлы, мьютексы), эти ресурсы могут утечь, так как деструктор объекта не будет вызван (объект не считается полностью созданным).
- Проблемы с наследованием и композицией: При создании объекта класса-наследника, если исключение выбрасывается в конструкторе базового класса или члена, деструкторы уже сконструированных подобъектов будут вызваны корректно, но деструктор самого производного класса — нет.
Решение — идиома RAII (Resource Acquisition Is Initialization): Ключевой принцип C++ для безопасной работы с ресурсами. Ресурс должен выделяться в конструкторе объекта и освобождаться в его деструкторе. Если конструктор завершается исключением, деструкторы уже полностью созданных подобъектов-членов (которые сами являются RAII-объектами) вызываются автоматически.
Плохой пример (утечка памяти):
class Problematic {
int* data1;
int* data2;
public:
Problematic(size_t size1, size_t size2) {
data1 = new int[size1]; // Выделили память
// Если здесь выбросить исключение...
if (size2 == 0) throw std::invalid_argument("size2 is zero");
// ...то память под data1 утечет, деструктор Problematic не вызовется.
data2 = new int[size2];
}
~Problematic() { delete[] data1; delete[] data2; }
};
Хороший пример (использование RAII через std::unique_ptr):
#include <memory>
#include <vector>
#include <stdexcept>
class Safe {
std::unique_ptr<int[]> data1; // RAII-обертка
std::vector<int> data2; // RAII-контейнер
public:
Safe(size_t size1, size_t size2)
: data1(std::make_unique<int[]>(size1)) // Инициализация члена
{
// Если исключение выброшено здесь, data1 автоматически освободится.
if (size2 == 0) {
throw std::invalid_argument("size2 is zero");
}
data2.resize(size2); // Инициализация второго члена
}
// Деструктор по умолчанию корректно освободит data1 и data2.
};
int main() {
try {
Safe s(100, 0); // Выбросит исключение, утечек не будет.
} catch (const std::exception& e) {
// Все ресурсы уже освобождены.
}
}
Вывод: Конструкторы должны либо полностью завершать работу, либо, в случае ошибки, выбрасывать исключение, предварительно полагаясь на деструкторы RAII-членов для очистки. Избегайте "голых" указателей и ручного управления ресурсами в конструкторах.