В чем особенность выброса исключения из конструктора в C++?

Ответ

Выброс исключения из конструктора — стандартный и безопасный способ сообщить об ошибке инициализации. Его ключевые особенности:

  1. Объект считается не созданным. Деструктор этого объекта не будет вызван, так как его жизненный цикл никогда не начинался.
  2. Частично созданные подобъекты разрушаются. Для уже сконструированных на момент исключения подобъектов (базовых классов и членов-объектов) деструкторы будут вызваны в обратном порядке. Это гарантирует язык.
  3. Исключение должно быть обработано снаружи. Конструктор не имеет возвращаемого типа, поэтому исключение — единственный способ сигнализировать о сбое.

Практический пример с управлением ресурсами:

#include <iostream>
#include <stdexcept>
#include <memory>

class FileHandle {
    FILE* m_file;
public:
    FileHandle(const char* filename) : m_file(nullptr) {
        m_file = fopen(filename, "r");
        if (!m_file) {
            // 1. Исключение в конструкторе
            throw std::runtime_error("Failed to open file");
        }
        std::cout << "File opened.n";
    }
    ~FileHandle() {
        if (m_file) {
            fclose(m_file);
            std::cout << "File closed.n";
        }
    }
};

class DatabaseConnection {
    std::string m_connectionString;
public:
    DatabaseConnection(const std::string& connStr) : m_connectionString(connStr) {
        // Имитация сбоя подключения
        throw std::runtime_error("Connection failed");
        std::cout << "DB connected.n";
    }
    ~DatabaseConnection() {
        std::cout << "DB disconnected.n";
    }
};

class ResourceManager {
    FileHandle m_file;        // Член 1
    DatabaseConnection m_db;  // Член 2
public:
    ResourceManager()
        : m_file("data.txt"), // Сконструируется успешно
          m_db("localhost")   // Выбросит исключение
    {
        std::cout << "ResourceManager fully constructed.n";
    }
    ~ResourceManager() {
        std::cout << "ResourceManager destroyed.n";
    }
};

int main() {
    try {
        ResourceManager rm; // Конструктор выбросит исключение
    } catch (const std::exception& e) {
        std::cout << "Caught: " << e.what() << "n";
    }
    return 0;
}

Вывод программы:

File opened.
File closed.
Caught: Connection failed

Что произошло:

  1. Сконструировался m_file.
  2. При конструировании m_db выброшено исключение.
  3. Для частично созданного объекта ResourceManager вызван деструктор ~FileHandle() (член m_file), но не вызван деструктор ~ResourceManager() (объект не создан).
  4. Исключение передано в main().

Ответ 18+ 🔞

А, ну это же классика, ебать мои старые костыли! Смотри, чувак, история про то, как объект пытается родиться, но у него нихуя не получается, и он начинает орать исключениями, как мартышка на банане.

Вот представь: конструктор — это такой доверенный чувак, который собирает тебе мебель из IKEA. Если у него винтик не влезает или инструкция на шведском, он не будет молча пихать хуй в пальто и делать кривую хрень. Он возьмёт и закричит: «Ёпта, тут хуйня!». Это и есть выброс исключения. Объект в итоге считается не созданным, вообще. Как будто его и не было.

Самое главное — деструктор этого несчастного недообъекта не вызовется. Ну логично же, раз он не родился, то и хоронить нечего. Но вот что важно: если внутри него уже успели собрать какие-то части (базовые классы или другие объекты-члены), то для них-то деструкторы вызовутся. Язык сам за этим следит, чтобы мусора не оставалось. Это как если наш сборщик мебели успел прикрутить ножки к столу, а потом понял, что столешница — говно. Он ножки-то аккуратно открутит и положит обратно в коробку. Порядок.

А теперь смотри на этот пиздопроебибна пример. Тут у нас два чудака: FileHandle, который открывает файл, и DatabaseConnection, который якобы к базе подключается.

class FileHandle {
    FILE* m_file;
public:
    FileHandle(const char* filename) : m_file(nullptr) {
        m_file = fopen(filename, "r");
        if (!m_file) {
            // 1. Исключение в конструкторе
            throw std::runtime_error("Failed to open file");
        }
        std::cout << "File opened.n";
    }
    ~FileHandle() {
        if (m_file) {
            fclose(m_file);
            std::cout << "File closed.n";
        }
    }
};

С файлом вроде всё просто. Открылся — красава. Не открылся — ёперный театр, кидаем исключение и не создаём объект.

А вот база данных — это уже хитрая жопа:

class DatabaseConnection {
    std::string m_connectionString;
public:
    DatabaseConnection(const std::string& connStr) : m_connectionString(connStr) {
        // Имитация сбоя подключения
        throw std::runtime_error("Connection failed");
        std::cout << "DB connected.n";
    }
    ~DatabaseConnection() {
        std::cout << "DB disconnected.n";
    }
};

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

А теперь главный герой — ResourceManager. Он пытается собрать в кучу и файл, и базу.

class ResourceManager {
    FileHandle m_file;        // Член 1
    DatabaseConnection m_db;  // Член 2
public:
    ResourceManager()
        : m_file("data.txt"), // Сконструируется успешно
          m_db("localhost")   // Выбросит исключение
    {
        std::cout << "ResourceManager fully constructed.n";
    }
    ~ResourceManager() {
        std::cout << "ResourceManager destroyed.n";
    }
};

Что происходит, когда мы пытаемся создать ResourceManager в main?

  1. Поехали инициализировать члены по порядку. m_file("data.txt") — ок, файл открылся. Вывелось "File opened.".
  2. Дальше очередь m_db("localhost"). А этот пидарас шерстяной сразу выкидывает "Connection failed".
  3. Всё, приехали. Конструктор ResourceManager не выполнился. Его деструктор НЕ вызывается. Объект — не жилец.
  4. Но! А что с тем файлом, который уже открыли? А вот тут язык не подведёт: для уже созданного члена m_file вызовется его деструктор ~FileHandle(), который файл закроет. Это чтобы утечек не было. Удивление пиздец, да? Но это правда работает.

Вывод-то какой будет?

File opened.
File closed.
Caught: Connection failed

Видишь? Файл открыли, потом (из-за исключения в другом члене) закрыли, а самого ResourceManager'а — ни хуя себе — как не было. Исключение улетело наверх, в catch.

Вот и вся магия. Кидай исключения из конструкторов не бздя. Это правильный, цивилизованный способ сказать: «Чувак, я не могу существовать в таком виде, давай по-новой». Главное — ресурсы за собой прибери, а язык поможет.