Неправильно ли переопределить макрос assert?
Некоторые люди рекомендуют использовать собственный макрос ASSERT (cond), а не переопределять существующий стандартный, assert (cond) макрос. Но это не помогает, если у вас много устаревшего кода, использующего assert(), который вы не хотите вносить изменения в исходный код, который вы хотите перехватить, упорядочить, сообщать об утверждении.
Я сделал
#undef assert
#define assert(cond) ... my own assert code ...
в таких ситуациях, как вышеприведенный код, уже использующий assert, что я хотел бы расширить поведение assert-failing - когда я хотел сделать что-то вроде
1) печать дополнительной информации об ошибке, чтобы сделать утверждения более полезными
2), автоматически вызывающий отладчик или дорожку стека при утверждении
... this, 2), может быть выполнено без переопределения assert путем реализации обработчика сигнала SIGABRT.
3) преобразование ошибок утверждения в броски.
... this, 3), обработчик сигнала не может быть выполнен - поскольку вы не можете исключить исключение С++ из обработчика сигнала. (По крайней мере, не надежно.)
Почему я могу сделать assert throw? Сложная обработка ошибок.
Я делаю это последнее обычно не потому, что хочу, чтобы программа продолжала работать после утверждения (хотя см. ниже), а потому, что мне нравится использовать исключения, чтобы обеспечить лучший контекст ошибок. Я часто делаю:
int main() {
try { some_code(); }
catch(...) {
std::string err = "exception caught in command foo";
std::cerr << err;
exit(1);;
}
}
void some_code() {
try { some_other_code(); }
catch(...) {
std::string err = "exception caught when trying to set up directories";
std::cerr << err;
throw "unhandled exception, throwing to add more context";
}
}
void some_other_code() {
try { some_other2_code(); }
catch(...) {
std::string err = "exception caught when trying to open log file " + logfilename;
std::cerr << err;
throw "unhandled exception, throwing to add more context";
}
}
и др.
т.е. обработчики исключений добавляют немного больше контекста ошибки, а затем ретронизуют.
Иногда у меня есть обработчики обработчиков исключений, например. to stderr.
Иногда у меня есть обработчики исключений, которые нажимают на стек сообщений об ошибках. (Очевидно, что это не сработает, когда проблема закончится.)
** Эти утвердительные исключения все еще выходят... **
Кто-то, кто прокомментировал это сообщение, @IanGoldby, сказал: "Идея утверждения, который не выходит, не имеет для меня никакого смысла".
Чтобы я не был ясен: обычно у меня есть такие исключения. Но в конце концов, возможно, не сразу.
например. вместо
#include <iostream>
#include <assert.h>
#define OS_CYGWIN 1
void baz(int n)
{
#if OS_CYGWIN
assert( n == 1 && "I don't know how to do baz(1) on Cygwin). Should not call baz(1) on Cygwin." );
#else
std::cout << "I know how to do baz(n) most places, and baz(n), n!=1 on Cygwin, but not baz(1) on Cygwin.\n";
#endif
}
void bar(int n)
{
baz(n);
}
void foo(int n)
{
bar(n);
}
int main(int argc, char** argv)
{
foo( argv[0] == std::string("1") );
}
только
% ./assert-exceptions
assertion "n == 1 && "I don't know how to do baz(1) on Cygwin). Should not call baz(1) on Cygwin."" failed: file "assert-exceptions.cpp", line 9, function: void baz(int)
/bin/sh: line 1: 22180 Aborted (core dumped) ./assert-exceptions/
%
вы можете сделать
#include <iostream>
//#include <assert.h>
#define assert_error_report_helper(cond) "assertion failed: " #cond
#define assert(cond) {if(!(cond)) { std::cerr << assert_error_report_helper(cond) "\n"; throw assert_error_report_helper(cond); } }
//^ TBD: yes, I know assert needs more stuff to match the definition: void, etc.
#define OS_CYGWIN 1
void baz(int n)
{
#if OS_CYGWIN
assert( n == 1 && "I don't know how to do baz(1) on Cygwin). Should not call baz(1) on Cygwin." );
#else
std::cout << "I know how to do baz(n) most places, and baz(n), n!=1 on Cygwin, but not baz(1) on Cygwin.\n";
#endif
}
void bar(int n)
{
try {
baz(n);
}
catch(...) {
std::cerr << "trying to accomplish bar by baz\n";
throw "bar";
}
}
void foo(int n)
{
bar(n);
}
int secondary_main(int argc, char** argv)
{
foo( argv[0] == std::string("1") );
}
int main(int argc, char** argv)
{
try {
return secondary_main(argc,argv);
}
catch(...) {
std::cerr << "main exiting because of unknown exception ...\n";
}
}
и получите несколько более значимые сообщения об ошибках
assertion failed: n == 1 && "I don't know how to do baz(1) on Cygwin). Should not call baz(1) on Cygwin."
trying to accomplish bar by baz
main exiting because of unknown exception ...
Мне не нужно объяснять, почему эти сообщения, чувствительные к контексту, могут быть более значимыми. Например. пользователь может не иметь ни малейшего представления о том, почему вызывается baz (1). Это может привести к ошибке погрешности - на cygwin вам может потребоваться вызвать cygwin_alternative_to_baz (1).
Но пользователь может понять, что такое "бар".
Да: это не гарантируется. Но, в этом отношении, утверждения не гарантируют работу, если они делают что-то более сложное, чем вызов в обработчике прерывания.
write(2,"error baz(1) has occurred",64);
и даже это не гарантирует работу (там есть безопасная ошибка в этом вызове.)
например. если malloc или sbrk не удалось.
Почему я могу сделать assert throw? Тестирование
Другая большая причина, по которой я иногда переопределял утверждение, заключалась в написании модульных тестов для устаревшего кода, кода, который использует assert для ошибок сигнала, которые мне не разрешено переписывать.
Если этот код является библиотечным кодом, тогда удобно переносить вызовы через try/catch. Посмотрите, обнаружена ли ошибка, и продолжайте.
О, черт возьми, я мог бы также признать это: иногда я написал этот код устаревшего. И я сознательно использовал assert() для сигнализации ошибок. Потому что я не мог положиться на пользователя, делающего try/catch/throw - на самом деле, часто один и тот же код должен использоваться в среде C/С++. Я не хотел использовать свой собственный макрос ASSERT, потому что, верьте или нет, ASSERT часто конфликтует. Я нахожу код, который усеян FOOBAR_ASSERT() и A_SPECIAL_ASSERT() уродливым. Нет... просто использование assert() само по себе является элегантным, работает в основном. И может быть расширен... если нормально переопределить assert().
Во всяком случае, является ли код, который использует assert(), моим или кем-то другим: иногда вы хотите, чтобы код терпит неудачу, вызывая SIGABRT или exit (1) - и иногда вы хотите его бросить.
Я знаю, как тестировать код, который терпит неудачу при выходе (a) или SIGABRT - что-то вроде
for all tests do
fork
... run test in child
wait
check exit status
но этот код медленный. Не всегда переносимый. И часто работает несколько тысяч раз медленнее
for all tests do
try {
... run test in child
} catch (... ) {
...
}
Это более опасно, чем просто контекстный контекст сообщения об ошибках, поскольку вы можете продолжить работу. Но вы всегда можете выбрать типы исключений для cactch.
Мета-наблюдение
Я с Андреем Александресчу, думая, что исключения - это самый известный способ сообщить о ошибках в коде, который хочет быть в безопасности. (Потому что программист не может забыть проверить код возврата ошибки.)
Если это правильно... если есть фазовый переход в сообщении об ошибках, от выхода (1)/сигналов/до исключений... все еще есть вопрос о том, как жить с устаревшим кодом.
И, в целом, существует несколько схем отчетов об ошибках. Если разные библиотеки используют разные схемы, как заставить их жить вместе.