C compiler asserts - how to implement?

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

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

#define MY_COMPILER_ASSERT(EXPRESSION) switch (0) {case 0: case (EXPRESSION):;}

Пример кода (который не компилируется).

#define DEFINE_A 1
#define DEFINE_B 1
MY_COMPILER_ASSERT(DEFINE_A == DEFINE_B);

Как я могу реализовать это, чтобы он не генерировал какой-либо код (чтобы минимизировать размер сгенерированных двоичных файлов)?

11 ответов

Решение

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

Ключевой трюк заключается в том, чтобы найти конструкцию, которая может быть оценена во время компиляции и может вызвать ошибку для некоторых значений. Один ответ - объявление массива не может иметь отрицательный размер. Использование typedef предотвращает выделение места при успехе и сохраняет ошибку при сбое.

Само сообщение об ошибке загадочно будет ссылаться на объявление отрицательного размера (GCC говорит, что "размер массива foo отрицателен"), поэтому вы должны выбрать имя для типа массива, которое намекает на то, что эта ошибка действительно является проверкой утверждения.

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

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

К сожалению, если проверка подтверждения находится во включенном файле, она все равно может столкнуться с проверкой по тому же номеру строки во втором включенном файле или по этому номеру строки в основном исходном файле. Мы могли бы оформить это с помощью макроса __FILE__, но она определена как строковая константа и не существует трюка препроцессора, который может превратить строковую константу обратно в часть имени идентификатора; не говоря уже о том, что допустимые имена файлов могут содержать символы, которые не являются допустимыми частями идентификатора.

Итак, я бы предложил следующий фрагмент кода:

/** A compile time assertion check.
 *
 *  Validate at compile time that the predicate is true without
 *  generating code. This can be used at any point in a source file
 *  where typedef is legal.
 *
 *  On success, compilation proceeds normally.
 *
 *  On failure, attempts to typedef an array type of negative size. The
 *  offending line will look like
 *      typedef assertion_failed_file_h_42[-1]
 *  where file is the content of the second parameter which should
 *  typically be related in some obvious way to the containing file
 *  name, 42 is the line number in the file on which the assertion
 *  appears, and -1 is the result of a calculation based on the
 *  predicate failing.
 *
 *  \param predicate The predicate to test. It must evaluate to
 *  something that can be coerced to a normal C boolean.
 *
 *  \param file A sequence of legal identifier characters that should
 *  uniquely identify the source file in which this condition appears.
 */
#define CASSERT(predicate, file) _impl_CASSERT_LINE(predicate,__LINE__,file)

#define _impl_PASTE(a,b) a##b
#define _impl_CASSERT_LINE(predicate, line, file) \
    typedef char _impl_PASTE(assertion_failed_##file##_,line)[2*!!(predicate)-1];

Типичное использование может быть что-то вроде:

#include "CAssert.h"
...
struct foo { 
    ...  /* 76 bytes of members */
};
CASSERT(sizeof(struct foo) == 76, demo_c);

В GCC ошибка утверждения будет выглядеть следующим образом:

$ gcc -c demo.c
demo.c: 32: ошибка: размер массива assertion_failed_demo_c_32 отрицательный
$

Следующие COMPILER_VERIFY(exp) макрос работает довольно хорошо.

// объединяем аргументы (после расширения аргументов)
#define GLUE (a, b) __GLUE (a, b)
#define __GLUE (a, b) a ## b

#define CVERIFY (expr, msg) typedef char GLUE (compiler_verify_, msg) [(expr)? (+1): (-1)]

#define COMPILER_VERIFY (exp) CVERIFY (exp, __LINE__)

Он работает как для C, так и для C++ и может использоваться везде, где разрешен typedef. Если выражение истинно, оно генерирует typedef для массива из 1 символа (что безвредно). Если выражение ложно, оно генерирует typedef для массива -1 символа, что обычно приводит к сообщению об ошибке. Выражение, данное как arugment, может быть любым, что вычисляется как константа времени компиляции (поэтому выражения, включающие sizeof(), работают нормально). Это делает его намного более гибким, чем

#if (expr)
#ошибка
#endif

где вы ограничены выражениями, которые могут быть оценены препроцессором.

Как сказал Леандер, статические утверждения добавляются в C++11, а теперь они есть.

static_assert(exp, message)

Например

#include "myfile.hpp"

static_assert(sizeof(MyClass) == 16, "MyClass is not 16 bytes!")

void doStuff(MyClass object) { }

Смотрите страницу cppreference на нем.

Использование '#error' является допустимым определением препроцессора, которое приводит к остановке компиляции на большинстве компиляторов. Вы можете просто сделать это, например, чтобы предотвратить компиляцию в отладке:


#ifdef DEBUG
#error Please don't compile now
#endif

Лучшая рецензия, которую я мог найти для статических утверждений в C, - это pixelbeat. Обратите внимание, что статические утверждения добавляются в C++ 0X и могут перейти в C1X, но это не будет какое-то время. Я не знаю, увеличат ли макросы в приведенной мной ссылке размер ваших двоичных файлов. Я подозреваю, что они не будут, по крайней мере, если вы компилируете на разумном уровне оптимизации, но ваш пробег может отличаться.

Я знаю, что вы заинтересованы в C, но взгляните на C++ static_bull boost. (Кстати, это, вероятно, становится доступным в C++1x.)

Мы сделали нечто подобное, опять же для C++:

#define COMPILER_ASSERT (expr) enum {ARG_JOIN (CompilerAssertAtLine, __LINE__) = sizeof (char [(expr)? +1: -1])}

Это работает только в C++, по-видимому. В этой статье обсуждается способ изменить его для использования в C.

Если ваш компилятор устанавливает макрос препроцессора, такой как DEBUG или NDEBUG, вы можете сделать что-то вроде этого (в противном случае вы можете установить это в Makefile):

#ifdef DEBUG
#define MY_COMPILER_ASSERT(EXPRESSION)   switch (0) {case 0: case (EXPRESSION):;}
#else
#define MY_COMPILER_ASSERT(EXPRESSION)
#endif

Затем ваш компилятор утверждает только для отладочных сборок.

Когда вы компилируете свои последние двоичные файлы, определите MY_COMPILER_ASSERT, чтобы он был пустым, чтобы его вывод не включался в результат. Определите это только так, как у вас есть для отладки.

Но на самом деле вы не сможете уловить каждое утверждение таким образом. Некоторые просто не имеют смысла во время компиляции (например, утверждение, что значение не является нулевым). Все, что вы можете сделать, это проверить значения других #defines. Я не совсем уверен, почему вы хотите это сделать.

Начиная с C11, static_assert доступен через <assert.h>. Начиная с C23, static_assert сам по себе является ключевым словом. https://en.cppreference.com/w/c/error/static_assert

      static_assert(2 + 2 == 4, "2+2 isn't 4"); // OK
static_assert(2 + 2 == 5, "2+2 isn't 4"); // Compile-time error

Если по какой-то причине вы вынуждены использовать старые стандарты C, для вдохновения ознакомьтесь с этой статьей: https://www.pixelbeat.org/programming/gcc/static_assert.html

Я нашел это, чтобы дать наименее запутанное сообщение об ошибке для GCC. У всего остального был суффикс об отрицательном размере или о чем-то другом:

#define STATIC_ASSERT(expr, msg)   \
typedef char ______Assertion_Failed_____##msg[1];  __unused \
typedef char ______Assertion_Failed_____##msg[(expr)?1:2] __unused

пример использования:

 unsigned char testvar;
 STATIC_ASSERT(sizeof(testvar) >= 8, testvar_is_too_small);

И сообщение об ошибке в gcc (ARM/GNU C Compiler: 6.3.1):

conflicting types for '______Assertion_Failed_____testvar_is_too_small'

Ну, вы могли бы использовать static asserts в буст-библиотеке.

Я верю, что они делают там, чтобы определить массив.

 #define MY_COMPILER_ASSERT(EXPRESSION) char x[(EXPRESSION)];

Если EXPRESSION имеет значение true, он определяет char x[1];это нормально. Если false, это определяет char x[0]; что незаконно.

Другие вопросы по тегам