在编译时验证宏参数大小

Verify macro argument size at compilation time

假设我有一个宏(关于 为什么 的更多详细信息在下面的 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 与位域一起使用,然后我的代码无法编译:

error: invalid application of 'sizeof' to bit-field

问题:如果宏参数的大小大于,比方说,uint32_t?[=24=,是否有一个选项可以在编译时检测]


P.S.

MY_MACRO 在实时嵌入式环境中的作用类似于 printf。该环境有一个 HW 记录器,最多可以接收 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! 

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

#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 时,会触发警告:

warning: implicit conversion loses integer precision: 'uint64_t' (aka 'unsigned long long') to 'uint32_t' (aka 'unsigned int') [-Wshorten-64-to-32]

注:

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

应启用适当的警告。

编辑:

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

#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)时,编译失败错误:

error: static_assert failed due to requirement '_Generic((1ULL), long long: 0, unsigned long long: 0, default: 1) && (_Generic((0), long long: 0, unsigned long long: 0, default: 1) && _Generic((-1), long long: 0, unsigned long long: 0, default: 1))' "64 bit parameters are not supported!"

三元表达式的类型 ?: 是它的第二个和第三个参数的公共类型(具有更小类型的整数提升)。因此,您的 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 宏不同。

Question: Is there an option to detect at the compilation time if the size of the macro argument larger than, let's say, 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 完全不兼容,您可能已经在此处获得了编译器 errors/warnings。如果不是,COUNT_ARGS 将 return 传递的参数数量。


除此之外,我们可以对可变参数列表中的每一项进行实际的、可移植的类型检查。使用 _Generic 检查单个项目的类型:

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

然后将结果传递给_Static_assert。然而,对于 5 个参数,我们需要检查 1 到 5 个项目。为此,我们可以 "chain" 一些宏:

#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 调用与参数数量相对应的 "chain" 中的适当宏:

_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 等令人讨厌的花括号格式标准。参见 Why use apparently meaningless do-while and if-else statements in macros?