47

在错误情况下,我想实现一个“断言”来防止编译,而不是在运行时失败。

我目前有一个这样定义的,效果很好,但会增加二进制文件的大小。

#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);

我该如何实现它以使其不生成任何代码(以最小化生成的二进制文件的大小)?

4

10 回答 10

50

纯标准 C 中的编译时断言是可能的,并且一点点预处理器技巧使它的使用看起来像在运行时使用assert().

关键技巧是找到一个可以在编译时评估并且可能导致某些值错误的构造。一个答案是数组的声明不能有负大小。使用 typedef 可以防止成功时分配空间,并保留失败时的错误。

错误消息本身将隐晦地引用负大小的声明(GCC 说“数组 foo 的大小为负”),因此您应该为数组类型选择一个名称,暗示此错误确实是一个断言检查。

另一个需要处理的问题是,在任何编译单元中只能为typedef特定类型名称指定一次。因此,宏必须为每个用法安排一个唯一的类型名称来声明。

我通常的解决方案是要求宏有两个参数。第一个是断言为真的条件,第二个是在幕后声明的类型名称的一部分。plinth 的答案暗示使用标记粘贴和__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 演示.c
demo.c:32:错误:数组“assertion_failed_demo_c_32”的大小为负
$
于 2009-04-30T22:41:46.383 回答
8

以下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 的地方使用。如果表达式为真,它会为 1 个字符的数组生成一个 typedef(这是无害的)。如果表达式为假,它会为 -1 个字符的数组生成 typedef,这通常会导致错误消息。作为参数给出的表达式可以是计算为编译时常量的任何内容(因此涉及 sizeof() 的表达式可以正常工作)。这使得它比

#if (表达式)
#错误
#万一

您仅限于可以由预处理器评估的表达式。

于 2009-04-30T15:43:47.403 回答
5

正如 Leander 所说,静态断言被添加到 C++11 中,现在它们已经添加了。

static_assert(exp, message)

例如

#include "myfile.hpp"

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

void doStuff(MyClass object) { }

请参阅上面的cppreference 页面

于 2014-04-17T10:44:59.960 回答
4

如果你的编译器设置了一个预处理器宏,如 DEBUG 或 NDEBUG,你可以做这样的事情(否则你可以在 Makefile 中设置):

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

然后,您的编译器只断言调试版本。

于 2009-04-30T14:47:49.753 回答
4

我能在 C 中找到的关于静态断言的最佳文章是在pixelbeat上。请注意,静态断言被添加到 C++ 0X 中,并且可能会添加到 C1X 中,但这不会持续一段时间。我不知道我提供的链接中的宏是否会增加二进制文件的大小。我怀疑他们不会,至少如果您以合理的优化水平进行编译,但您的里程可能会有所不同。

于 2009-04-30T15:07:26.803 回答
3

使用“#error”是一个有效的预处理器定义,它会导致编译在大多数编译器上停止。例如,您可以这样做,以防止在调试中编译:


#ifdef DEBUG
#error Please don't compile now
#endif
于 2009-04-30T14:51:26.640 回答
3

我知道您对 C 感兴趣,但请看一下 boost 的 C++ static_assert。(顺便说一句,这很可能在 C++1x 中可用。)

我们也为 C++ 做了类似的事情:

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

显然,这只适用于 C++。 本文讨论了一种修改它以在 C 中使用的方法。

于 2009-04-30T15:09:15.117 回答
2

当您编译最终的二进制文件时,将 MY_COMPILER_ASSERT 定义为空白,以便其输出不包含在结果中。仅按照您的调试方式定义它。

但实际上,您无法以这种方式捕捉每一个断言。有些只是在编译时没有意义(比如断言一个值不为空)。您所能做的就是验证其他#defines 的值。我不确定你为什么要这样做。

于 2009-04-30T14:43:28.780 回答
0

我发现这为 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'
于 2018-04-24T16:50:11.390 回答
-2

好吧,您可以使用static assertsboost library 中的

我相信他们在那里做的是定义一个数组。

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

如果 EXPRESSION 为真,它定义char x[1];,这是可以的。如果为假,则定义char x[0];哪个是非法的。

于 2009-04-30T15:08:25.820 回答