О чем нужно помнить, бросая исключение из конструктора
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Бросание исключений из конструктора: критические правила
Бросание исключения из конструктора — мощный механизм сигнализации об ошибке инициализации, но требует осторожности. Нарушение этих правил приводит к утечкам ресурсов, неопределённому поведению и сложным багам.
1. Правило: конструктор либо полностью создаёт объект, либо выбрасывает исключение
Это основной принцип strong exception guarantee (строгая гарантия исключений):
class FileHandler {
private:
FILE* file;
public:
FileHandler(const std::string& filename) {
file = fopen(filename.c_str(), "r");
if (!file) {
// Объект находится в невалидном состоянии
throw std::runtime_error("Cannot open file: " + filename);
}
}
~FileHandler() {
if (file) fclose(file);
}
};
// Использование:
FileHandler handler("nonexistent.txt"); // выбросит исключение
// Объект не создан - деструктор НЕ вызывается!
Важно: если выбросить исключение, деструктор класса НЕ вызовется. Все ресурсы, выделенные до исключения, должны быть освобождены в самом конструкторе.
2. Освобождение ресурсов перед выбросом исключения
class ResourceManager {
private:
int* buffer;
FILE* file;
public:
ResourceManager(int size, const std::string& filename) {
// Выделяем память
buffer = new int[size];
// Открываем файл
file = fopen(filename.c_str(), "r");
if (!file) {
// ПРАВИЛЬНО: освобождаем память перед исключением
delete[] buffer;
buffer = nullptr;
throw std::runtime_error("Cannot open file");
}
}
~ResourceManager() {
if (buffer) delete[] buffer;
if (file) fclose(file);
}
};
// НЕПРАВИЛЬНО:
class BadResourceManager {
private:
int* buffer;
FILE* file;
public:
BadResourceManager(int size, const std::string& filename) {
buffer = new int[size]; // выделили
file = fopen(filename.c_str(), "r");
if (!file) {
// ОШИБКА: выбросили исключение без освобождения buffer
throw std::runtime_error("Cannot open file");
// buffer утечёт в памяти!
}
}
};
3. Использование RAII (Resource Acquisition Is Initialization)
RAII — идиом C++, где ресурсы выделяются в конструкторе и освобождаются в деструкторе. Это автоматизирует управление ресурсами:
class RaiiResourceManager {
private:
std::unique_ptr<int[]> buffer;
std::unique_ptr<FILE, decltype(&fclose)> file;
public:
RaiiResourceManager(int size, const std::string& filename)
: file(nullptr, &fclose) {
// Выделяем через unique_ptr - автоматическое освобождение
buffer = std::make_unique<int[]>(size);
// Открываем файл
FILE* f = fopen(filename.c_str(), "r");
if (!f) {
// unique_ptr автоматически удалит buffer при исключении
throw std::runtime_error("Cannot open file");
}
file.reset(f);
}
// Деструктор не нужен - unique_ptr всё очистит автоматически
};
// Использование:
try {
RaiiResourceManager rm(1000, "config.txt");
// если исключение - buffer и file освобождаются автоматически
} catch (const std::exception& e) {
std::cout << "Error: " << e.what() << std::endl;
}
Это безопаснее: даже если забыть про очистку, деструкторы RAII объектов позаботятся о ней.
4. Исключения в инициализаторе членов
Исключение может выброситься в списке инициализации, и важно правильно обработать это:
class Complex {
private:
SubObject sub1;
SubObject sub2;
int* buffer;
public:
Complex(const std::string& filename)
: sub1("config1"),
sub2("config2") { // исключение может выброситься здесь
buffer = new int[1000];
// Если sub2() выбросит исключение:
// - sub1 уже построен (его деструктор будет вызван)
// - sub2 не построен (его деструктор НЕ вызовется)
// - buffer не выделен (исключение выброшено до этой строки)
// - Деструктор Complex НЕ вызовется
}
~Complex() {
delete[] buffer; // НЕ вызовется, если исключение в конструкторе
}
};
Решение: используй RAII и выделяй ресурсы ДО инициализаторов:
class SafeComplex {
private:
SubObject sub1; // будет разрушен автоматически
SubObject sub2;
std::unique_ptr<int[]> buffer;
public:
SafeComplex(const std::string& filename)
: sub1("config1"),
sub2("config2"),
buffer(std::make_unique<int[]>(1000)) {
// Если что-то выбросит исключение:
// - sub1 и sub2 будут разрушены автоматически
// - buffer будет освобождён unique_ptr
}
};
5. Исключения в производных классах
При наследовании конструктор базового класса может выбросить исключение:
class Base {
public:
Base(const std::string& filename) {
FILE* f = fopen(filename.c_str(), "r");
if (!f) {
throw std::runtime_error("Base init failed");
}
fclose(f);
}
};
class Derived : public Base {
private:
std::unique_ptr<int[]> buffer;
public:
Derived(const std::string& filename)
: Base(filename), // может выбросить исключение
buffer(nullptr) {
buffer = std::make_unique<int[]>(1000);
}
};
// Если Base::Base() выбросит исключение:
// - buffer никогда не создастся
// - Derived::~Derived() НЕ вызовется
// - Но Base::~Base() вызовется для разрушения базовой части
6. Функция-try блок для конструктора
Для перехвата исключений из инициализаторов членов и базовых классов:
class FileContainer {
private:
std::string filename;
public:
// function-try блок
FileContainer(const std::string& fn)
try : filename(fn) {
// основное тело конструктора
if (filename.empty()) {
throw std::invalid_argument("Empty filename");
}
}
catch (const std::exception& e) {
// Можно залогировать ошибку
std::cerr << "Construction failed: " << e.what() << std::endl;
// ВАЖНО: исключение будет re-thrown автоматически!
// Конструктор всё равно "не удаст" - объект не создан
}
};
7. Исключения в многопоточной среде
class ThreadPool {
private:
std::vector<std::thread> threads;
public:
ThreadPool(int numThreads) {
for (int i = 0; i < numThreads; ++i) {
try {
threads.emplace_back([this]() { this->workerThread(); });
}
catch (...) {
// Если создание потока не удалось
// Уже созданные потоки нужно остановить
for (auto& t : threads) {
if (t.joinable()) t.join();
}
throw; // re-throw исключение
}
}
}
~ThreadPool() {
for (auto& t : threads) {
if (t.joinable()) t.join();
}
}
private:
void workerThread() { /* ... */ }
};
Чеклист при исключении из конструктора
- 1️ Освободи все ресурсы перед выбросом исключения (или используй RAII)
- 2️ Помни, что деструктор НЕ будет вызван если конструктор выбросил исключение
- 3️ Используй unique_ptr / shared_ptr вместо сырых указателей
- 4️ Проверь наследование - исключения в базовых классах
- 5️ Логируй информацию об ошибке перед выбросом
- 6️ Гарантируй strong exception guarantee - либо успех, либо никаких побочных эффектов
- 7️ Тестируй ошибки инициализации - это критические сценарии
Примеры антипаттернов
// ❌ Утечка ресурса
class Bad1 {
public:
Bad1() {
ptr = new int[100];
throw std::runtime_error("Error"); // ptr утечёт
}
~Bad1() { delete[] ptr; }
private:
int* ptr;
};
// ❌ Частично инициализированный объект
class Bad2 {
public:
Bad2() {
member1.init(); // успех
member2.init(); // может выбросить
// если member2 выбросит - member1 уже инициализирован
// но деструктор Bad2 не будет вызван
}
private:
Obj member1, member2;
};
// ✅ Правильный подход
class Good {
public:
Good()
: member1(std::make_unique<Obj>()),
member2(std::make_unique<Obj>()) {
// member1 и member2 - умные указатели
// автоматически очистятся при исключении
}
private:
std::unique_ptr<Obj> member1;
std::unique_ptr<Obj> member2;
};
Помни: исключение из конструктора означает отсутствие объекта. Все ресурсы должны быть освобождены, и никакие гарантии целостности состояния не требуются — объекта просто нет.