Какие проблемы возникают при выбросе исключения в конструкторе?

«Какие проблемы возникают при выбросе исключения в конструкторе?» — вопрос из категории C++ Core, который задают на 25% собеседований C/C++ Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Выброс исключения в конструкторе — стандартный и часто правильный способ сигнализировать об ошибке инициализации. Однако это создает специфические проблемы, которые нужно учитывать.

Основные проблемы:

  1. Частично сконструированный объект: Если исключение выброшено после выделения некоторых ресурсов (память, файлы, мьютексы), эти ресурсы могут утечь, так как деструктор объекта не будет вызван (объект не считается полностью созданным).
  2. Проблемы с наследованием и композицией: При создании объекта класса-наследника, если исключение выбрасывается в конструкторе базового класса или члена, деструкторы уже сконструированных подобъектов будут вызваны корректно, но деструктор самого производного класса — нет.

Решение — идиома 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-членов для очистки. Избегайте "голых" указателей и ручного управления ресурсами в конструкторах.