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





Да, на самом деле есть. Вам нужно будет написать настраиваемую функцию assert самостоятельно, поскольку assert() в C++ - это в точности assert() из C, со встроенной «функцией» abort(). К счастью, это на удивление просто.
Assert.hh
template <typename X, typename A>
inline void Assert(A assertion)
{
if ( !assertion ) throw X();
}
Вышеупомянутая функция вызовет исключение, если предикат не выполняется. Тогда у вас будет возможность поймать исключение. Если вы не поймаете исключение, будет вызван terminate(), который завершит программу аналогично abort().
Вы можете спросить, а как насчет оптимизации утверждения, когда мы создаем для продакшена. В этом случае вы можете определить константы, которые будут обозначать, что вы создаете для производства, а затем ссылаться на константу при использовании Assert().
debug.hh
#ifdef NDEBUG
const bool CHECK_WRONG = false;
#else
const bool CHECK_WRONG = true;
#endif
main.cc
#include<iostream>
struct Wrong { };
int main()
{
try {
Assert<Wrong>(!CHECK_WRONG || 2 + 2 == 5);
std::cout << "I can go to sleep now.\n";
}
catch( Wrong e ) {
std::cerr << "Someone is wrong on the internet!\n";
}
return 0;
}
Если CHECK_WRONG является константой, тогда вызов Assert() будет скомпилирован в производственной среде, даже если утверждение не является константным выражением. Есть небольшой недостаток в том, что, ссылаясь на CHECK_WRONG, мы вводим немного больше. Но взамен мы получаем преимущество в том, что мы можем классифицировать различные группы утверждений и включать и отключать каждую из них по своему усмотрению. Так, например, мы могли бы определить группу утверждений, которые мы хотим включить даже в производственном коде, а затем определить группу утверждений, которые мы хотим видеть только в разрабатываемых сборках.
Функция Assert() эквивалентна вводу
if ( !assertion ) throw X();
но это ясно указывает на намерение программиста: сделать утверждение. Утверждения также легче найти с помощью этого подхода, как и обычные assert().
Дополнительные сведения об этой технике см. В книге Бьярна Страуструпа «Язык программирования C++ 3e», раздел 24.3.7.2.
В чем добавленная стоимость шаблонизации аргумента assert вместо того, чтобы сделать его int? В любом случае его нужно будет преобразовать в int.
@Greg Rogers Bjarne Stroustrup предлагает решение проблемы, о которой вы упомянули: вы можете добавить настраиваемый объект, который вы хотите использовать, в качестве дополнительного параметра Assert (). Обратной стороной является то, что Assert () больше не будет выглядеть как assert () из-за дополнительного параметра.
@JohnMcG: параметр шаблона дает мне дополнительную гибкость. вы можете передать все, что угодно функции Assert (), для которой определен operator! () и которая возвращает что-то, что можно преобразовать в bool. работают логические значения, целые числа и некоторые настраиваемые объекты.
это очень просто, код предполагает, что это не что иное, как обернутое исключение. но я надеялся, что люди попытаются выйти за рамки очевидного. подобные тирады можно сказать и в старом assert (), но он служит очень полезной задаче. этот трюк просто пытается устранить недостаток старого assert () без дополнительных затрат.
Помимо генерируемого исключения, это служит больше концепцией, чем другой код, который мы привыкли видеть.
Правильно ли я считаю, что это не может работать с проверкой некопируемых интеллектуальных указателей? Например: std::unique_ptr<int> p; Assert( p ); Разве этот код не должен приводить к сбою компиляции (из-за копии), что означает, что единственный способ его использовать - это выполнить p.get (), а затем потерять большую часть универсальности?
Теперь, когда я думаю об этом, это можно решить, установив по умолчанию A на bool. Что вы думаете?
Поскольку это доказательство концепции работает, вы можете просто использовать макрос с поддержкой шаблонов, чтобы отразить точное использование и вывод assert(), ссылаясь на утверждать и Замена текстовых макросов. Самая сложная часть (если вы используете GCC) будет получить имя файла без суффикса. К счастью, есть фрагмент кода от @RichardHodges, в котором вы можете почерпнуть вдохновение из здесь.
Утверждения в C / C++ выполняются только в отладочных сборках. Так что во время выполнения этого не произойдет. Как правило, утверждения должны отмечать вещи, которые, если они происходят, указывают на ошибку и обычно показывают предположения в вашем коде и т. д.
Если вы хотите иметь код, который проверяет наличие ошибок во время выполнения (в выпуске), вам, вероятно, следует использовать исключения, а не утверждения, поскольку они предназначены для этого. Ваш ответ в основном обертывает генератор исключения в синтаксисе assert. Хотя это будет работать, в этом нет особого преимущества, которое я могу видеть по сравнению с простым выбросом исключения в первую очередь.
Утверждения, работающие или не выполняющиеся в производственных сборках, являются опцией компилятора. Они определенно запускаются в gcc по умолчанию, независимо от параметров оптимизации.
Я лично считаю хорошей идеей держать утверждения в режиме выпуска. Вы можете предпочесть это случайным сбоям, когда ваш клиент использует приложение ...
Особенно, если ваша программа приведет к неопределенному поведению, если это утверждение станет ложным; это позволит клиенту, по крайней мере, дать более ценное сообщение об ошибке и упростить шаги воспроизведения; учитывая, что это НИКОГДА не должно было случиться; и вы думали (и гарантировали), что это не так.
@DarkShikari Формулировка здесь, к сожалению, расплывчата. assert() становится нерабочим только в том случае, если определен NDEBUG - и я не думаю, что какой-либо компилятор автоматически определяет это на основе заданных параметров оптимизации и / или отладки. Может быть, IDE делают, и это сбивает с толку. Как бы то ни было, спор тогда сводится к тому, определять ли NDEBUG в выпущенной сборке, конфликтует ли это с каким-либо другим кодом #ifdefd, нужно ли нам писать собственный макрос для получения более точного поведения и т. д.
функции отчетов об ошибках в glib использует подход продолжения после утверждения. glib - это базовая библиотека независимости от платформы, которую использует Gnome (через GTK). Вот макрос, который проверяет предварительное условие и печатает трассировку стека, если предварительное условие не выполняется.
#define RETURN_IF_FAIL(expr) do { \
if (!(expr)) \
{ \
fprintf(stderr, \
"file %s: line %d (%s): precondition `%s' failed.", \
__FILE__, \
__LINE__, \
__PRETTY_FUNCTION__, \
#expr); \
print_stack_trace(2); \
return; \
}; } while(0)
#define RETURN_VAL_IF_FAIL(expr, val) do { \
if (!(expr)) \
{ \
fprintf(stderr, \
"file %s: line %d (%s): precondition `%s' failed.", \
__FILE__, \
__LINE__, \
__PRETTY_FUNCTION__, \
#expr); \
print_stack_trace(2); \
return val; \
}; } while(0)
Вот функция, которая печатает трассировку стека, написанную для среды, использующей набор инструментов gnu (gcc):
void print_stack_trace(int fd)
{
void *array[256];
size_t size;
size = backtrace (array, 256);
backtrace_symbols_fd(array, size, fd);
}
Вот как вы бы использовали макросы:
char *doSomething(char *ptr)
{
RETURN_VAL_IF_FAIL(ptr != NULL, NULL); // same as assert(ptr != NULL), but returns NULL if it fails.
if ( ptr != NULL ) // Necessary if you want to define the macro only for debug builds
{
...
}
return ptr;
}
void doSomethingElse(char *ptr)
{
RETURN_IF_FAIL(ptr != NULL);
}
@wilhelmtell Я считаю, что это лучшее решение, потому что я не согласен с использованием исключения. Исключение должно быть для чего-то особенного, однако обработка Assert - нет. Кроме того, если вы планируете оставить Asserts включенными в сборке релиза, что, я думаю, вам следует делать, если производительность не является жизненно важной, этот метод будет более эффективным. Вы также можете настроить его так, чтобы ваша программа не выходила из строя, когда это произойдет, и вы получите информацию обратно. Предлагаю прочитать главу книги «Прагматичный программист» об утверждениях и исключениях. Плюс эта ссылка.
Вот что у меня в assert.h (Mac OS 10.4):
#define assert(e) ((void) ((e) ? 0 : __assert (#e, __FILE__, __LINE__)))
#define __assert(e, file, line) ((void)printf ("%s:%u: failed assertion `%s'\n", file, line, e), abort(), 0)
Исходя из этого, замените вызов abort () на throw (исключение). И вместо printf вы можете отформатировать строку в сообщение об ошибке исключения. В итоге получится что-то вроде этого:
#define assert(e) ((void) ((e) ? 0 : my_assert (#e, __FILE__, __LINE__)))
#define my_assert( e, file, line ) ( throw std::runtime_error(\
std::string(file:)+boost::lexical_cast<std::string>(line)+": failed assertion "+e))
Я не пробовал скомпилировать, но смысл вы поняли.
Примечание: вам необходимо убедиться, что заголовок «исключение» всегда включен, а также заголовок boost (если вы решите использовать его для форматирования сообщения об ошибке). Но вы также можете сделать my_assert функцией и объявить только ее прототип. Что-то вроде:
void my_assert( const char* e, const char* file, int line);
И реализуйте его где-нибудь, где вы можете свободно включать все нужные заголовки.
Оберните его в какой-нибудь #ifdef DEBUG, если он вам нужен, или нет, если вы всегда хотите запускать эти проверки.
Так что остается спорить, лучше ли макрос или шаблонная функция. Кроме того, оператор последовательности означает, что операнды могут выполняться в любом произвольном порядке. Я сомневаюсь, что это то, что вы имеете в виду в __asert ().
Функция «__assert» в первом блоке исходит из моего системного заголовка (0S 10.4). Надеюсь, они знают, что делают. Макрос упрощает получение информации о ФАЙЛАХ и СТРОКАХ. Но в какой-то момент становится довольно косметическим ...
_set_error_mode(_OUT_TO_MSGBOX);
поверьте, эта функция может вам помочь.
Похоже, это функция только для Windows: связь
И, по крайней мере, вы должны были объяснить, какой заголовок / компилятор / что-то еще нужно для его использования.
Если вы хотите бросить символьную строку с информацией об утверждении: http://xll8.codeplex.com/SourceControl/latest#xll/ensure.h
Поскольку вы можете получить только созданные по умолчанию исключения, от этого не будет особой пользы, поскольку вы не можете транспортировать какие-либо данные времени выполнения на сайт отслеживания ...