Ответ
Выброс исключения из конструктора — стандартный и безопасный способ сообщить об ошибке инициализации. Его ключевые особенности:
- Объект считается не созданным. Деструктор этого объекта не будет вызван, так как его жизненный цикл никогда не начинался.
- Частично созданные подобъекты разрушаются. Для уже сконструированных на момент исключения подобъектов (базовых классов и членов-объектов) деструкторы будут вызваны в обратном порядке. Это гарантирует язык.
- Исключение должно быть обработано снаружи. Конструктор не имеет возвращаемого типа, поэтому исключение — единственный способ сигнализировать о сбое.
Практический пример с управлением ресурсами:
#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
Что произошло:
- Сконструировался
m_file. - При конструировании
m_dbвыброшено исключение. - Для частично созданного объекта
ResourceManagerвызван деструктор~FileHandle()(членm_file), но не вызван деструктор~ResourceManager()(объект не создан). - Исключение передано в
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?
- Поехали инициализировать члены по порядку.
m_file("data.txt")— ок, файл открылся. Вывелось"File opened.". - Дальше очередь
m_db("localhost"). А этот пидарас шерстяной сразу выкидывает"Connection failed". - Всё, приехали. Конструктор
ResourceManagerне выполнился. Его деструктор НЕ вызывается. Объект — не жилец. - Но! А что с тем файлом, который уже открыли? А вот тут язык не подведёт: для уже созданного члена
m_fileвызовется его деструктор~FileHandle(), который файл закроет. Это чтобы утечек не было. Удивление пиздец, да? Но это правда работает.
Вывод-то какой будет?
File opened.
File closed.
Caught: Connection failed
Видишь? Файл открыли, потом (из-за исключения в другом члене) закрыли, а самого ResourceManager'а — ни хуя себе — как не было. Исключение улетело наверх, в catch.
Вот и вся магия. Кидай исключения из конструкторов не бздя. Это правильный, цивилизованный способ сказать: «Чувак, я не могу существовать в таком виде, давай по-новой». Главное — ресурсы за собой прибери, а язык поможет.