Зло ли переопределить утверждение?

Неправильно ли переопределить макрос 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)/сигналов/до исключений... все еще есть вопрос о том, как жить с устаревшим кодом.

И, в целом, существует несколько схем отчетов об ошибках. Если разные библиотеки используют разные схемы, как заставить их жить вместе.

Ответ 1

Переопределение стандартного макроса является уродливой идеей, и вы можете быть уверены в техническом поведении undefined, но в конце макросы - это просто замены исходного кода, и трудно понять, как это может вызвать проблемы, если утверждение заставляет вашу программу выйти.

Тем не менее, ваша предполагаемая подстановка не может быть надежно использована, если какой-либо код в блоке трансляции после определения вашего определения переопределяет assert, что предполагает необходимость определенного порядка включений и т.д. - проклятый хрупкий.

Если ваш assert заменяет код, который не exit, вы открываете новые проблемы. Есть случаи патологических краев, когда ваши идеи о метании вместо этого могут потерпеть неудачу, например:

int f(int n)
{
    try
    {
        assert(n != 0);
        call_some_library_that_might_throw(n);
    }
    catch (...)
    {
        // ignore errors...
    }
    return 12 / n;
}

Выше, значение 0 для n запускает сбой приложения вместо того, чтобы остановить его с помощью звукового сообщения об ошибке: любое объяснение в заброшенном сообщении не будет видно.

Я с Андреем Александресчу, думая, что исключения - это самый известный способ сообщить о ошибках в коде, который хочет быть в безопасности. (Потому что программист не может забыть проверить код возврата ошибки.)

Я не помню, как Андрей говорил об этом - у вас есть цитата? Он, конечно, очень тщательно подумал о том, как создавать объекты, которые поощряют надежную обработку исключений, но я никогда не слышал/не видел, что он говорит о том, что в некоторых случаях утверждать о приостановке программы является неуместным. Утверждения являются обычным способом принудительного применения инвариантов - там определенно существует линия, в отношении которой потенциальные утверждения могут быть продолжены и которые не могут, но с одной стороны этой строки продолжают оставаться полезными.

Выбор между возвратом значения ошибки и использованием исключений является традиционной основой для аргумента/предпочтения, о котором вы упоминаете, поскольку они более законно альтернативы.

Если это правильно... если есть фазовый переход в сообщении об ошибках, от выхода (1)/сигналов/до исключений... все еще есть вопрос о том, как жить с устаревшим кодом.

Как и выше, вы не должны пытаться перенести все существующие exit()/assert и т.д. на исключения. Во многих случаях не будет смысла значимо продолжать обработку, а выброс исключений вызывает сомнения в том, что проблема будет записана правильно и приведет к предполагаемому завершению.

И, в целом, существует несколько схем отчетов об ошибках. Если разные библиотеки используют разные схемы, как заставить их жить вместе.

Если это становится реальной проблемой, вы обычно выбираете один подход и завершаете несоответствующие библиотеки слоем, который предоставляет вам необходимую обработку ошибок.

Ответ 2

Я написал приложение, которое работает во встроенной системе. В первые дни я посыпал утверждения через код либерально, якобы для документирования условий в коде, которые должны быть невозможны (но в нескольких местах как ленивая проверка ошибок).

Оказалось, что утверждения иногда ударялись, но никто не видел, чтобы сообщение выводилось на консоль, содержащую номер файла и строки, потому что консольный последовательный порт вообще не был подключен ни к чему. Позже я переопределил макрос assert, чтобы вместо вывода сообщения на консоль он отправил сообщение по сети в журнал ошибок.

Независимо от того, считаете ли вы, что переопределение утверждения является "злом", это хорошо работает для нас.

Ответ 3

Если вы включаете заголовки/библиотеки, которые используют assert, вы можете столкнуться с неожиданным поведением, иначе компилятор позволит вам сделать это, чтобы вы могли это сделать.

Мое предложение, основанное на личном мнении, заключается в том, что в любом случае вы можете определить свое собственное утверждение без необходимости переопределять существующее. Вы никогда не получаете дополнительную выгоду от переопределения существующего над определением нового с новым именем.