在编译时验证宏参数大小
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_t
或 int64_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_t
和 uint32_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?
假设我有一个宏(关于 为什么 的更多详细信息在下面的 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_t
或 int64_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_t
和uint32_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?