← Назад к вопросам

О чем нужно помнить, бросая исключение из конструктора

2.0 Middle🔥 131 комментариев
#STL контейнеры и алгоритмы

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Бросание исключений из конструктора: критические правила

Бросание исключения из конструктора — мощный механизм сигнализации об ошибке инициализации, но требует осторожности. Нарушение этих правил приводит к утечкам ресурсов, неопределённому поведению и сложным багам.

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;
};

Помни: исключение из конструктора означает отсутствие объекта. Все ресурсы должны быть освобождены, и никакие гарантии целостности состояния не требуются — объекта просто нет.

О чем нужно помнить, бросая исключение из конструктора | PrepBro