Должны ли исключения быть связаны в C++?
Я только что закончил работу над C++- программой, в которой реализовал свои собственные исключения (хотя и производные от std::exception). Практика, которую я применил, когда одно исключение вызывает цепную реакцию, распространяя ошибку вверх и порождая другие исключения, заключается в объединении сообщения об ошибке на каждом соответствующем шаге по всем модулям (классы чтения). Т.е. само старое исключение удаляется и создается новое исключение, но с более длинным сообщением об ошибке.
Возможно, это сработало для моей маленькой программы, но в итоге я не был очень доволен своим подходом. Во-первых, номера строк (хотя в настоящий момент не применяются) и имена файлов не сохраняются, за исключением последнего исключения; и действительно, эта информация представляет наибольший интерес в первом исключении.
Я полагаю, что это могло бы быть лучше обработано путем объединения исключений; т.е. старое исключение предоставляется в конструкторе нового исключения. Но как это будет реализовано? Разве исключения не умирают, когда они выходят за рамки метода, тем самым мешая использовать указатели исключений? А как скопировать и сохранить исключение, если исключение может быть любого производного класса?
В конечном итоге это заставляет меня задуматься о том, является ли объединение исключений в C++ такой хорошей идеей. Возможно, нужно просто создать одно исключение, а затем добавить к нему дополнительные данные (как я делал, но, вероятно, гораздо лучше)?
Каков ваш ответ на это? Должны ли исключения, вызванные другим, быть объединены в цепочку, чтобы сохранить своего рода "трассировку исключений" - и как это должно быть реализовано? - или следует использовать одно исключение и приложить к нему дополнительные данные - и как это сделать?
4 ответа
Необходимо скопировать данные из объекта исключения в цепочку, если вы хотите, чтобы он пережил catch
блок, который получает его, кроме throw;
, (Что включает в себя, например, если это catch
блок выходит через throw obj;
.)
Это может быть сделано путем помещения данных для сохранения в кучу и реализации swap
(move
в C++0x) на ваших личных данных внутри исключения, например.
Конечно, вы должны быть осторожны при использовании кучи с исключениями… но, опять же, в большинстве современных ОС чрезмерное использование памяти полностью предотвращает new
от броска, к лучшему или к худшему. Хороший запас памяти и исключение исключений из цепочки после полного разрушения должны обеспечить ее безопасность.
struct exception_data { // abstract base class; may contain anything
virtual ~exception_data() {}
};
struct chained_exception : std::exception {
chained_exception( std::string const &s, exception_data *d = NULL )
: data(d), descr(s) {
try {
link = new chained_exception;
throw;
} catch ( chained_exception &prev ) {
swap( *link, prev );
} // catch std::bad_alloc somehow...
}
friend void swap( chained_exception &lhs, chained_exception &rhs ) {
std::swap( lhs.link, rhs.link );
std::swap( lhs.data, rhs.data );
swap( lhs.descr, rhs.descr );
}
virtual char const *what() const throw() { return descr.c_str(); }
virtual ~chained_exception() throw() {
if ( link && link->link ) delete link; // do not delete terminator
delete data;
}
chained_exception *link; // always on heap
exception_data *data; // always on heap
std::string descr; // keeps data on heap
private:
chained_exception() : link(), data() {}
friend int main();
};
void f() {
try {
throw chained_exception( "humbug!" );
} catch ( std::exception & ) {
try {
throw chained_exception( "bah" );
} catch ( chained_exception &e ) {
chained_exception *ep = &e;
for ( chained_exception *ep = &e; ep->link; ep = ep->link ) {
std::cerr << ep->what() << std::endl;
}
}
}
try {
throw chained_exception( "meh!" );
} catch ( chained_exception &e ) {
for ( chained_exception *ep = &e; ep->link; ep = ep->link ) {
std::cerr << ep->what() << std::endl;
}
}
}
int main() try {
throw chained_exception(); // create dummy end-of-chain
} catch( chained_exception & ) {
// body of main goes here
f();
}
выходной (соответственно сварливый):
bah
humbug!
meh!
Поскольку этот вопрос был задан, в стандарт были внесены заметные изменения в C++11. Я постоянно упускаю это в обсуждениях об исключениях, но следующий подход, вложив исключения, делает свое дело:
использование std::nested_exception
а также std::throw_with_nested
Здесь и здесь в Stackru описывается, как вы можете получить обратную трассировку ваших исключений внутри кода без необходимости в отладчике или громоздкой регистрации, просто написав соответствующий обработчик исключений, который будет перебрасывать вложенные исключения.
Поскольку вы можете сделать это с любым производным классом исключений, вы можете добавить много информации к такой обратной трассировке! Вы также можете взглянуть на мой MWE на GitHub, где обратная трассировка будет выглядеть примерно так:
Library API: Exception caught in function 'api_function'
Backtrace:
~/Git/mwe-cpp-exception/src/detail/Library.cpp:17 : library_function failed
~/Git/mwe-cpp-exception/src/detail/Library.cpp:13 : could not open file "nonexistent.txt"
Вы можете посмотреть на это: http://www.boost.org/doc/libs/1_43_0/libs/exception/doc/boost-exception.html
Это несколько другой подход к тому, что MS сделала в C#, но, похоже, он соответствует вашим требованиям.
Другая идея состоит в том, чтобы добавить соответствующие данные в ваш объект исключения, а затем использовать голые throw;
заявление, чтобы перебросить его. Я думаю, что информация о стеке сохраняется в этом случае, и поэтому вы все равно будете знать исходный источник исключения, но тестирование было бы хорошей идеей.
Бьюсь об заклад, так как то, доступна ли вообще какая-либо информация о стеке, определяется реализацией, что реализации будут еще более широко варьироваться в зависимости от того, сохраняется ли она каким-либо образом после простого throw;
заявление.