Компилятор C утверждает - как реализовать? - PullRequest
41 голосов
/ 30 апреля 2009

Я бы хотел реализовать "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);

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

Ответы [ 10 ]

41 голосов
/ 01 мая 2009

Возможно утверждение времени компиляции в чистом стандартном 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: error: size of array `assertion_failed_demo_c_32' is negative
$
7 голосов
/ 30 апреля 2009

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

// combine arguments (after expanding arguments)
#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)
#error
#endif

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

4 голосов
/ 30 апреля 2009

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

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

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

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

4 голосов
/ 30 апреля 2009

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

4 голосов
/ 30 апреля 2009

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

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

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

3 голосов
/ 17 апреля 2014

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

static_assert(exp, message)

Например

#include "myfile.hpp"

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

void doStuff(MyClass object) { }

См. Страницу cppreference .

2 голосов
/ 30 апреля 2009

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

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

1 голос
/ 30 апреля 2009

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


#ifdef DEBUG
#error Please don't compile now
#endif
0 голосов
/ 24 апреля 2018

Я обнаружил, что это дает наименее запутанное сообщение об ошибке для 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'
0 голосов
/ 30 апреля 2009

Ну, вы можете использовать static asserts в библиотеке повышения .

Я думаю, что они там делают, это определяют массив.

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

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

...