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

«В чем особенность выброса исключения из конструктора в C++?» — вопрос из категории C++ Core, который задают на 25% собеседований C/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().