【问题标题】:Verify macro argument size at compilation time在编译时验证宏参数大小
【发布时间】:2019-09-22 12:01:51
【问题描述】:

假设我有一个宏(有关为什么的更多详细信息,请参阅下面的 P.S. 部分)

void my_macro_impl(uint32_t arg0, uint32_t arg1, uint32_t arg2);

...

#define MY_MACRO(arg0, arg1, arg2)       my_macro_impl((uint32_t)(arg0), (uint32_t)(arg1), (uint32_t)(arg2))

要使用此宏的硬件是小端并使用 32 位架构,因此所有指针都达到(包括)32 位宽度。我的目标是在错误传递uint64_tint64_t 参数时警告用户。

我正在考虑像这样使用sizeof

#define MY_MACRO(arg0, arg1, arg2)       do \
                                         {  \
                                             static_assert(sizeof(arg0) <= sizeof(uint32_t));  \
                                             static_assert(sizeof(arg1) <= sizeof(uint32_t));  \
                                             static_assert(sizeof(arg2) <= sizeof(uint32_t));  \
                                             my_macro_impl((uint32_t)(arg0), (uint32_t)(arg1), (uint32_t)(arg2));  \
                                         } while (0)

但是用户可以使用带有位字段的MY_MACRO,然后我的代码无法编译:

错误:“sizeof”对位域的应用无效

问题:是否可以在编译时检测宏参数的大小是否大于uint32_t


附言

MY_MACRO 在实时嵌入式环境中的作用类似于printf。这个环境有一个硬件记录器,它最多可以接收 5 个参数,每个参数应该是 32 位。目标是保留printf 的标准格式。格式字符串是离线解析的,解析器很清楚每个参数都是 32 位的,所以它会根据格式字符串中的%... 进行转换。可能的用法如下。

不需要的用法:

uint64_t time = systime_get();
MY_MACRO_2("Starting execution at systime %llx", time); // WRONG! only the low 32 bits are printed. I want to detect it and fail the compilation.

预期用途:

uint64_t time = systime_get();
MY_MACRO_3("Starting execution at systime %x%x", (uint32_t)(time >> 32), (uint32_t)time); // OK! 

【问题讨论】:

  • 使用sizeof 0 + (arg0)会有什么不同吗?
  • @melpomene 假设 32 位架构 (godbolt.org/z/PwI9mZ),这可能会有所不同。
  • @melpomene ...唯一的缺点是当传递的参数是数组warning: sizeof on pointer operation will return size of 'int *' instead of 'int [30]' [-Wsizeof-array-decay]时会发出警告

标签: c sizeof bit-fields c11


【解决方案1】:

以下方法可能适用于这种需求:

#define CHECK_ARG(arg)                  _Generic((arg), \
                                                 int64_t  : (arg),  \
                                                 uint64_t : (arg),  \
                                                 default  : (uint32_t)(arg))

那么,MY_MACRO 可以定义为

#define MY_MACRO(a0, a1, a2)       do \
                                   {  \
                                       uint32_t arg1 = CHECK_ARG(a0);  \
                                       uint32_t arg2 = CHECK_ARG(a1);  \
                                       uint32_t arg3 = CHECK_ARG(a2);  \
                                       my_macro_impl(arg1, arg2, arg3);\
                                   } while (0)

在这种情况下,当传递例如uint64_t 时,会触发警告:

警告:隐式转换丢失整数精度:'uint64_t'(又名 'unsigned long long')到'uint32_t'(又名'unsigned int') [-Wshorten-64-to-32]

注意:

其他类型如double、128/256 位类型可以类似处理。

应启用适当的警告。

编辑:

Lundin的评论和answer的启发,上面提出的解决方案可以很容易地修改为可移植版本,这将导致编译错误而不仅仅是编译器警告。

#define CHECK_ARG(arg)          _Generic((arg),         \
                                         int64_t  : 0,  \
                                         uint64_t : 0,  \
                                         default  : 1)

所以MY_MACRO可以修改为

#define MY_MACRO(a0, a1, a2)       do \
                                   {  \
                                       _Static_assert(CHECK_ARG(a1) && \
                                                      CHECK_ARG(a2) && \
                                                      CHECK_ARG(a3),   \
                                                      "64 bit parameters are not supported!"); \
                                       my_macro_impl((uint32_t)(a1), (uint32_t)(a2), (uint32_t)(a3)); \
                                   } while (0)

这一次,当传递uint64_t参数MY_MACRO(1ULL, 0, -1)时,编译失败并出现错误

错误: static_assert 由于要求 '_Generic((1ULL), long long: 0, unsigned long long: 0, 默认: 1) && (_Generic((0), long long: 0, unsigned long long: 0, 默认值: 1) && _Generic((-1), long long: 0, unsigned long long: 0, default: 1))' "不支持64位参数!"

【讨论】:

  • 因为 OP 仅指定检测 int64_tuint64_t(而不是编译器可能提供的广泛扩展类型),这对我来说看起来不错。不过,它确实需要 C11。
  • @JohnBollinger 对。 128/256 位类型可以以相同的方式处理。是的,_Generic 需要 c11。
  • 这如何解决问题?它依赖于特定的 gcc 警告。 _Generic 没有添加任何内容,它明确允许 64 位指针通过,然后也允许各种其他错误类型通过。
  • @Lundin 请阅读附言。部分。目的是实现一个记录器并允许它打印指针/整数,类似于printf 方法。这个硬件上的指针是 32 位的,所以它可能是指向任何东西(uint64_t、函数、数组)的指针,只要硬件记录器有完整的值。
  • 我想我可以看到这个解决方案的五个问题。见godbolt.org/z/SOO76h#1 它不拒绝double args。 #2 gcc 不拒绝宽度为 33 位的位域。 #3 clang 可能会错误地(?)拒绝宽度为 1 的位域。#4 旧版本的 clang(例如 2015 年的 3.7)不会错误地拒绝 const uint64_t#5 需要 C11,因此不太便携。
【解决方案2】:

三元?: 表达式的类型是其第二个和第三个参数的通用类型(具有较小类型的整数提升)。因此,MY_MACRO 的以下版本将在 32 位架构中工作:

static_assert(sizeof(uint32_t) == sizeof 0, ""); // sanity check, for your machine

#define MY_MACRO(arg0, arg1, arg2) \
    do {  \
        static_assert(sizeof(0 ? 0 : (arg0)) == sizeof 0, "");  \
        static_assert(sizeof(0 ? 0 : (arg1)) == sizeof 0, "");  \
        static_assert(sizeof(0 ? 0 : (arg2)) == sizeof 0, "");  \
        my_macro_impl((uint32_t)(arg0), (uint32_t)(arg1), (uint32_t)(arg2));  \
    } while (0)

此外,此解决方案应适用于所有版本的 C 和 C++(如有必要,可使用合适的 static_assert 定义)。

请注意,与 OP 的原始宏一样,此宏具有 函数语义,因为参数只计算一次,这与臭名昭著的 MAX 宏不同。

【讨论】:

  • 这是一个有趣的方法,但是如果参数不是算术类型(例如指针),这将不起作用:error: invalid argument type 'void *' to unary expression。一元 + 只能应用于算术类型。
  • 更新: 答案已修改为使用三元运算符。我最初的回答建议使用一元 + 进行促销,但 OP 指出这不适用于指针。一个快速的解决方法是使用二进制文件+ 添加0。但是根据严格的规则,这将在void * 指针上失败。 (一些编译器,例如 gcc,允许这样做,但是您需要在宏定义中添加 pragma 以抑制警告消息。)
  • 请注意?: 带有隐式类型提升——第二个和第三个参数的类型根据“通常的算术转换”进行转换,这意味着?: 强制输入变为int 在小整数类型的情况下。所以如果你给这个宏提供uint8_t_Bool 之类的东西,它就不会被发现。此外(正如所指出的),它不能移植到 int 的大小与 int32_t 不同的 8 位或 16 位系统。
  • 您对位域的问题是正确的。与_Generic 一起使用非常棘手。使用整数提升的方法应该适用于所有情况。
【解决方案3】:

问题:是否可以在编译时检测宏参数的大小是否大于 uint32_t?

可移植地执行此操作的唯一方法是使用_Generic 生成编译器错误。如果您希望错误美观且可读,请将_Generic 的结果提供给_Static_assert,以便您可以输入自定义字符串作为编译器消息。

您的规格似乎是这样的:

  • 一切都必须是编译时检查。
  • 该宏可以获取 1 到 5 个任意类型的参数。
  • 只有int32_tuint32_t 是允许的类型。

这意味着你必须编写一个可变参数宏,它必须接受 1 到 5 个参数。


这样的宏可以这样写:

#define COUNT_ARGS(...) ( sizeof((uint32_t[]){__VA_ARGS__}) / sizeof(uint32_t) )

#define MY_MACRO(...)                                                           \
  _Static_assert(COUNT_ARGS(__VA_ARGS__)>0 && COUNT_ARGS(__VA_ARGS__)<=5,       \
                 "MY_MACRO: Wrong number of arguments");                        

COUNT_ARGS 创建一个临时复合文字,其中包含您为宏提供的尽可能多的对象。如果它们与uint32_t 完全不兼容,您可能已经在此处收到编译器错误/警告。如果没有,COUNT_ARGS 将返回传递的参数数量。


这样,我们就可以对变量参数列表中的每一项进行实际的、可移植的类型检查。用_Generic检查单个项目的类型:

#define CHECK(arg) _Generic((arg), uint32_t: 1, int32_t: 1, default: 0)

然后将结果传递给_Static_assert。但是,对于 5 个参数,我们需要检查 1 到 5 个项目。为此,我们可以“链接”多个宏:

#define CHECK(arg) _Generic((arg), uint32_t: 1, int32_t: 1, default: 0)

#define CHECK_ARGS1(arg1,...) CHECK(arg1)
#define CHECK_ARGS2(arg2,...) (CHECK(arg2) && CHECK_ARGS1(__VA_ARGS__,0))
#define CHECK_ARGS3(arg3,...) (CHECK(arg3) && CHECK_ARGS2(__VA_ARGS__,0))
#define CHECK_ARGS4(arg4,...) (CHECK(arg4) && CHECK_ARGS3(__VA_ARGS__,0))
#define CHECK_ARGS5(arg5,...) (CHECK(arg5) && CHECK_ARGS4(__VA_ARGS__,0))

每个宏检查传递给它的第一个参数,然后将其余参数(如果有)转发给下一个宏。尾随的 0 用于关闭有关可变参数宏所需的其余参数的 ISO C 警告。

我们可以将对这些的调用烘焙到一个_Static_assert 中,该_Static_assert 调用与参数数量相对应的“链”中的正确宏:

_Static_assert(COUNT_ARGS(__VA_ARGS__) == 1 ? CHECK_ARGS1(__VA_ARGS__,0) :    \
               COUNT_ARGS(__VA_ARGS__) == 2 ? CHECK_ARGS2(__VA_ARGS__,0) :    \
               COUNT_ARGS(__VA_ARGS__) == 3 ? CHECK_ARGS3(__VA_ARGS__,0) :    \
               COUNT_ARGS(__VA_ARGS__) == 4 ? CHECK_ARGS4(__VA_ARGS__,0) :    \
               COUNT_ARGS(__VA_ARGS__) == 5 ? CHECK_ARGS5(__VA_ARGS__,0) : 0, \
               "MY_MACRO: incorrect type in parameter list " #__VA_ARGS__);   \

完整代码及使用示例:

#include <stdint.h>

#define COUNT_ARGS(...) ( sizeof((uint32_t[]){__VA_ARGS__}) / sizeof(uint32_t) )

#define CHECK(arg) _Generic((arg), uint32_t: 1, int32_t: 1, default: 0)

#define CHECK_ARGS1(arg1,...) CHECK(arg1)
#define CHECK_ARGS2(arg2,...) (CHECK(arg2) && CHECK_ARGS1(__VA_ARGS__,0))
#define CHECK_ARGS3(arg3,...) (CHECK(arg3) && CHECK_ARGS2(__VA_ARGS__,0))
#define CHECK_ARGS4(arg4,...) (CHECK(arg4) && CHECK_ARGS3(__VA_ARGS__,0))
#define CHECK_ARGS5(arg5,...) (CHECK(arg5) && CHECK_ARGS4(__VA_ARGS__,0))

#define MY_MACRO(...)                                                           \
do {                                                                            \
  _Static_assert(COUNT_ARGS(__VA_ARGS__)>0 && COUNT_ARGS(__VA_ARGS__)<=5,       \
                 "MY_MACRO: Wrong number of arguments");                        \
  _Static_assert(COUNT_ARGS(__VA_ARGS__) == 1 ? CHECK_ARGS1(__VA_ARGS__,0) :    \
                 COUNT_ARGS(__VA_ARGS__) == 2 ? CHECK_ARGS2(__VA_ARGS__,0) :    \
                 COUNT_ARGS(__VA_ARGS__) == 3 ? CHECK_ARGS3(__VA_ARGS__,0) :    \
                 COUNT_ARGS(__VA_ARGS__) == 4 ? CHECK_ARGS4(__VA_ARGS__,0) :    \
                 COUNT_ARGS(__VA_ARGS__) == 5 ? CHECK_ARGS5(__VA_ARGS__,0) : 0, \
                 "MY_MACRO: incorrect type in parameter list " #__VA_ARGS__);   \
} while(0)


int main (void)
{
//MY_MACRO();                          // won't compile, "empty initializer braces"
//MY_MACRO(1,2,3,4,5,6);               // static assert "MY_MACRO: Wrong number of arguments"
  MY_MACRO(1);                         // OK, all parameters int32_t or uint32_t
  MY_MACRO(1,2,3,4,5);                 // OK, -"-
  MY_MACRO(1,(uint32_t)2,3,4,5);       // OK, -"-
//MY_MACRO(1,(uint64_t)2,3,4,5);       // static assert "MY_MACRO: incorrect type..."
//MY_MACRO(1,(uint8_t)2,3,4,5);        // static assert "MY_MACRO: incorrect type..."
}

这应该是 100% 可移植的,并且不依赖于编译器提供超出标准要求的额外诊断。

旧的do-while(0) 技巧是为了兼容诸如if(x) MY_MACRO(1) else 之类的icky 式大括号格式标准。见Why use apparently meaningless do-while and if-else statements in macros?

【讨论】:

  • 关键是可以使用 32 位或更少 (8, 16) 的参数... 64 位会导致问题,因为只有低 32 位会写入硬件。所以期望的行为是只对 64 位参数失败
  • @AlexLop。这可以很容易地适应允许或阻止任何类型(包括指针类型、结构、数组等)。只需重写 CHECK 宏。
猜你喜欢
  • 1970-01-01
  • 2012-10-06
  • 1970-01-01
  • 2013-11-15
  • 2013-05-23
  • 2012-01-13
  • 2017-08-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多