Пользовательский макрос утверждения C++

Я наткнулся на информативную статью: http://cnicholson.net/2009/02/stupid-c-tricks-adventures-in-assert/, который указал на большое количество проблем, существующих в моем текущем наборе отладочных макросов.

Полный код финальной версии макроса приведен ближе к концу статьи, если вы перейдете по ссылке.

Общая форма в представленном виде выглядит так (кто-нибудь, пожалуйста, поправьте меня, если я ошибаюсь в переносе):

#ifdef DEBUG
#define ASSERT(cond) \  
    do \  
    { \  
        if (!(cond)) \  
        { \  
            ReportFailure(#cond, __FILE__, __LINE__, 0); \
            HALT(); \
        } \  
    } while(0)  
#else  
#define ASSERT(cond) \  
    do { (void)sizeof(cond); } while(0) 

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

Во-первых, вы не можете использовать этот макрос с тернарным оператором (то есть cond?ASSERT(x):func()), и было предложено заменить if() тернарным оператором и некоторыми круглыми скобками, а также оператором запятой. Позже другой комментатор сообщил следующее:

#ifdef DEBUG
#define ASSERT(x) ((void)(!(x) && assert_handler(#x, __FILE__, __LINE__) && (HALT(), 1)))
#else
#define ASSERT(x) ((void)sizeof(x))
#endif

Я подумал, что использование логического и && в этом случае особенно разумно, и мне кажется, что эта версия более гибкая, чем версия, использующая if или даже троичную ?:. Еще лучше то, что возвращаемое значение assert_handler можно использовать для определения необходимости остановки программы. Хотя я не уверен, почему это (HALT(), 1), а не просто HALT().

Есть ли какие-то особые недостатки второй версии, которые я упустил из виду? Это избавляет от do{ } while(0), обернутых вокруг макросов, но здесь это кажется ненужным, потому что нам не нужно иметь дело с ifs.

Что вы думаете?


person Steven Lu    schedule 09.03.2011    source источник
comment
См. также мой ответ ниже: stackoverflow.com/a/21827201/216063   -  person Gregory Pakosz    schedule 26.04.2014
comment
Обратите внимание, что sizeof не применимо к функциям и элементам битовых полей. Как предлагается здесь stackoverflow.com/questions/4030959/, вместо этого вы можете использовать ((void)(true ? 0 : ((x), void(), 0)));.   -  person anxieux    schedule 25.08.2018


Ответы (3)


В стандартной библиотеке C и C++ assert — это макрос, который необходим для работы в качестве функции. Частью этого требования является то, что пользователи должны иметь возможность использовать его в выражениях. Например, со стандартным assert я могу сделать

int sum = (assert(a > 0), a) + (assert(b < 0), b);

который функционально аналогичен

assert(a > 0 && b < 0)
int sum = a + b;

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

Это сразу означает, что если кто-то хочет, чтобы их собственный макрос ASSERT имитировал стандартное поведение и удобство использования assert, то использование if или do { } while (0) в определении ASSERT исключено. В этом случае можно ограничиться выражениями, то есть использовать либо оператор ?:, либо короткие логические операторы.

Конечно, если не заботиться о создании стандартного кастомного ASSERT, то можно использовать что угодно, в том числе и if. Связанная статья, кажется, даже не рассматривает этот вопрос, что довольно странно. На мой взгляд, макрос assert, похожий на функцию, определенно полезнее, чем макрос, не похожий на функцию.

Что касается (HALT(), 1)... Это сделано так, потому что оператор && требует корректного аргумента. Возвращаемое значение HALT() может не представлять допустимый аргумент для &&. Насколько я знаю, это может быть void, а это означает, что простое HALT() просто не будет компилироваться в качестве аргумента &&. (HALT(), 1) всегда оценивается как 1 и имеет тип int, который всегда является допустимым аргументом для &&. Таким образом, (HALT(), 1) всегда является допустимым аргументом для && независимо от типа HALT().

Ваш последний комментарий о do{ } while(0) не имеет особого смысла. Смысл включения макроса в do{ } while(0) состоит в том, чтобы иметь дело с внешними if, а не с if внутри определения макроса. Вам всегда приходится иметь дело с внешними if, поскольку всегда есть шанс, что ваш макрос будет использоваться во внешнем if. В последнем определении do{ } while(0) не требуется, поскольку этот макрос является выражением. А будучи выражением, уже естественно не имеет проблем с внешними ifs. Так что ничего с ними делать не надо. Более того, как я сказал выше, включение его в do{ } while(0) полностью лишило бы его цели, превратив его в не-выражение.

person AnT    schedule 09.03.2011
comment
«На мой взгляд, макрос assert, похожий на функцию, определенно полезнее, чем макрос, не похожий на функцию». - Почему? Когда это полезно? Я думаю, что утверждения всегда должны стоять отдельно в коде и никогда не использоваться в выражениях. - person Konrad Rudolph; 10.03.2011
comment
Я бы сказал, что это лучше, потому что утверждение можно использовать в выражении, а также в обычном автономном режиме. Больше возможностей для пользователя. @AndreyT, спасибо за кристально ясное понимание этого различия, которое я как бы упустил из виду. Сначала это выглядело довольно круто, но теперь оказалось, что конструкция do{}while(0) совсем не очень полезна. По правде говоря, я использовал довольно много макросов, которые состояли только из одного большого оператора if, что на данный момент явно не подходит. - person Steven Lu; 10.03.2011

Для полноты картины я опубликовал вставную реализацию макроса assert из двух файлов на C++:

#include <pempek_assert.h>

int main()
{
  float min = 0.0f;
  float max = 1.0f;
  float v = 2.0f;
  PEMPEK_ASSERT(v > min && v < max,
                "invalid value: %f, must be between %f and %f", v, min, max);

  return 0;
}

Подскажет вам:

Assertion 'v > min && v < max' failed (DEBUG)
  in file e.cpp, line 8
  function: int main()
  with message: invalid value: 2.000000, must be between 0.000000 and 1.000000

Press (I)gnore / Ignore (F)orever / Ignore (A)ll / (D)ebug / A(b)ort:

Где

  • (I)gnore: игнорировать текущее утверждение
  • Игнорировать (F) навсегда: запомнить файл и строку, в которой сработало утверждение, и игнорировать их для оставшегося выполнения программы.
  • Ignore (A)ll: игнорировать все оставшиеся утверждения (все файлы и строки)
  • (D)ebug: взломать отладчик, если он подключен, иначе abort() (в Windows система предложит пользователю подключить отладчик)
  • A(b)ort: немедленно позвонить abort()

Подробнее об этом можно узнать там:

Надеюсь, это поможет.

person Gregory Pakosz    schedule 17.02.2014
comment
Спасибо, это довольно аккуратно. Пока я буду придерживаться своего собственного решения, хотя скажу, что оно выглядит уродливым взломом по сравнению с тем, что у вас есть здесь. - person Steven Lu; 17.02.2014
comment
Не стесняйтесь пинговать меня, если вы когда-нибудь решите переключиться. Я тестировал его на Mac, Linux, Windows, iOS и Android. - person Gregory Pakosz; 17.02.2014

Хотя я не уверен, почему это (HALT(), 1), а не просто HALT().

Я предполагаю, что HALT может быть макросом (или другим замещающим именем) для exit. Скажем, мы хотели использовать exit(1) для нашей команды HALT. exit возвращает void, который нельзя использовать в качестве второго аргумента &&. Если вы используете оператор запятой, который оценивает свой первый аргумент, затем оценивает и возвращает значение своего второго аргумента, у нас есть целое число (1), чтобы вернуться к &&, даже если мы никогда не достигнем этой точки, потому что HALT() заставит нас остановиться задолго до этого.

По сути, любая функция, которая заменяет HALT, вероятно, будет иметь возвращаемое значение void, потому что для нее не имеет смысла возвращать какое-либо значение. Мы могли заставить его возвращать int, просто ради макроса, но если мы уже ковыряемся с макросом, еще немного хакерства не повредит, не так ли?

person Chris Lutz    schedule 09.03.2011
comment
Таким образом, это был способ осчастливить компилятор и вызвать выход как побочный эффект (завершение программы! Должен сказать, довольно побочный эффект), задав ему возвращаемое значение. Аккуратный. - person Steven Lu; 10.03.2011