2013-02-12 2 views
12

Неправильно ли переопределить макрос утверждения?Зло ли переопределить утверждение?

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

Я сделал

#undef assert 
#define assert(cond) ... my own assert code ... 

в таких ситуациях, как выше - код уже использует утверждать, что я хотел бы расширить Assert-провал поведение - когда я хотел сделать такие вещи, как

1) печать дополнительной информации об ошибке, чтобы сделать утверждение более полезными

2) автоматически, вызывая отладчик или стеку дорожку на утверждают

.. это, 2), может быть сделано без переопределения assert путем реализации обработчика сигнала SIGABRT.

3) преобразование ошибок утверждения в броски.

... это, 3), не может быть выполнено обработчиком сигнала - поскольку вы не можете исключить исключение C++ из обработчика сигнала. (По крайней мере, не надежно.)

Почему я могу сделать 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"; 
    } 
} 

т.д.

Т.е. обработчики исключений добавляют немного больше контекста ошибки, а затем ретронизуют.

Иногда у меня есть обработчики обработчиков исключений, например. к stderr.

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

** Эти утверждают исключения до сих пор выйти ... **

кто-нибудь комментировал этот пост, @IanGoldby, сказал " Идея утверждения, который не выходит, не имеет для меня никакого смысла ».

Lest I was not clear: Обычно у меня есть такие исключения. Но в конце концов, возможно, не сразу.

E.g.вместо

#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 ... 

Я не должен объяснить, почему это продолжение ext чувствительные сообщения об ошибках могут быть более значимыми. . пользователь может не иметь ни малейшего представления о том, почему вызывается baz (1). Возможно, это ошибка пограммы - на cygwin вам может потребоваться вызвать cygwin_alternative_to_baz (1).

Но пользователь может понять, что такое «бар».

Да: это не гарантируется. Но, в этом отношении, утверждения не гарантируют работу, если они делают что-то более сложное, чем вызов в обработчике прерывания.

write(2,"error baz(1) has occurred",64); 

и даже то, что не гарантирует работу (есть безопасная ошибка в этом вызове.)

Э.Г. если malloc или sbrk не удалось.

Почему я могу сделать запрос assert? Тестирование

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

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

О, черт возьми, я мог бы также признать это: иногда я написал этот код устаревшего. И я сознательно использовал assert() для сигнализации ошибок. Потому что я не мог положиться на пользователя, делающего try/catch/throw - на самом деле, часто один и тот же код должен использоваться в среде C/C++. Я не хотел использовать свой собственный макрос ASSERT, потому что, верьте или нет, ASSERT часто конфликтует. Я нахожу код, который усеян FOOBAR_ASSERT() и A_SPECIAL_ASSERT() уродливым. Нет ... просто использование assert() само по себе является элегантным, работает в основном. И может быть расширен ... если нормально переопределить assert().

В любом случае, является ли код, который использует assert() моим или кем-то другим: иногда вы хотите, чтобы код терпел неудачу, вызывая SIGABRT или exit (1) - и иногда вы хотите, чтобы он выбрасывал.

Я знаю, как проверить код, который не по выходу (а) или 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.

Мета-наблюдения

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

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

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

+4

Это может быть более подходящим для программистов, поскольку вы ищете консенсус по * культуре программирования *, а не по решению конкретной проблемы программирования. – dmckee

+3

Вы должны определить свой собственный ASSERT. Переопределение чего-то из стандартного технически может привести к неопределенному поведению. Если у вас много старого кода, который необходимо изменить, вам нужен хороший редактор кода или IDE с инструментами рефакторинга. –

+1

Хотя я согласен с тем, что собственное определение может избежать некоторых проблем, я не верю, что переопределение 'assert()' является тем злом, что можно сделать это на единицу компиляции. Я сделал это самостоятельно на определенных платформах, это не обескураживает языковой спецификацией: См. 7.2.1.1 Макрос утверждений в C99 Обоснование 5.10: http://www.open-std.org/jtc1/sc22/ wg14/www/C99RationaleV5.10.pdf – Sam

ответ

10

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

При этом ваша предполагаемая замена не может быть надежно использована, если какой-либо код в блоке трансляции после определения вашего определения переопределяет 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 и т. Д. На исключения. Во многих случаях не будет смысла значимо продолжать обработку, а выброс исключений просто вызывает сомнения в том, что проблема будет правильно записана и приведет к предполагаемому завершению.

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

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

+0

Идея утверждения, который не выходит, не имеет для меня никакого смысла. Утверждение должно использоваться только для документирования состояния, которое, по мнению разработчиков, должно быть истинным в определенной точке кода, если в кодовой логике не существует недостатка. Они предназначены для обнаружения ошибок, а не недопустимых данных или ошибок времени выполнения. Если у вас есть ошибка (кроме особых условий), вы больше не имеете представления о целостности процесса или его данных, поэтому все, что вы можете сделать, это остановить. (Если вам повезет, вы можете управлять выходами в безопасное состояние до остановки.) –

+0

@IanGoldby: yes - Я согласен, но в этом вопросе конкретно постулируется это использование. Я не хотел, чтобы меня затащили в объяснение ситуаций, в которых программисты склонны выбирать один или другой, - просто проиллюстрировали, что предлагаемая техника не была даже надежной. –

+0

@TonyD: Андрей воспользовался исключениями для обработки ошибок в презентации, которую он дал на языке программирования D, на котором я присутствовал и сделал заметки на: http://blog.andy.glew.ca/2009/07/andrei-alexandrescu-case -for-d-visited.html // не относится к Андрею, но Андрей активно связан с D, а D говорит: http://dlang.org/overview.html Обработка исключений. Все больше опыта работы с обработкой исключений показывает, что это лучший способ обработки ошибок, чем традиционный метод использования кодов ошибок и глобальных переменных errno. // Исключения D лучше, чем C++, но я думаю, что C++ можно использовать. –

2

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

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

+1

Просто потому, что компилятор проглатывает определенный код, это не значит, что он легальный, стандартный мудрый – PlasmaHH

+1

@PlasmaHH Я никогда не говорил, что это так. Иногда такие переопределения имеют свои объективные варианты использования, и ответ на этот вопрос будет обсуждаться. –

4

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

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

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

+0

@ Ian Goldby - спасибо, что напомнил мне: я сделал почти то же самое. В одном случае я написал базу данных для ПК, который управлял несколькими дисплеями в киосках. Макросы assertacy(), полученные от поставщика компилятора, записывались на основной монитор ПК. Пришлось изменить их для записи в журнал, который можно было бы получить по телефонной линии. –

Смежные вопросы