C23 引入了属性
[[reproducible]]
和 [[unsequenced]]
。
引发问题的是,当没有可用的函数定义时,编译器无法洞察函数。这会阻止几乎所有编译器优化(除非使用 LTO)。
考虑以下示例:
// note: this is redundant, because [[unsequenced]] implies [[reproducible]]
int square(int x) [[reproducible]] [[unsequenced]];
int arr[] = {
square(2),
square(2),
square(3),
square(3),
};
即使编译器没有
square
的定义,也允许执行两种优化:
[[reproducible]]
,连续调用两次square(2)
会产生相同的结果,并且编译器可以决定仅调用 square(2)
一次[[unsequenced]]
,所以可以按任意顺序调用 square
,编译器甚至可以决定在程序启动时只计算一次 square(2)
。它还可以决定在 square(3)
之前评估 square(2)
,如果这样更有效的话。此类优化也可以通过在标头中定义函数
inline
来实现,并让编译器可以自行推断这些属性。然而,对于复杂的函数,由于增加了编译速度,使所有内容都inline
是不可行的。
更严格的解释,请参阅 C23 标准工作草案 N3096 §6.7.12.7 函数类型的标准属性。
[[reproducible]]
此属性断言函数是可重现函数,这意味着
Effectless 限制函数可以修改的状态。如果修改任何非本地状态,则只能通过传递给它的指针来发生。例如,如果
void to_upper_case(char* str)
函数仅修改局部变量和 str
的内容,则它是 effectless。 (直观上,该函数没有可观察到的副作用。)
幂等意味着多次调用该函数与调用一次具有相同的效果。例如,我们可以调用
to_upper_case(s); to_upper_case(s);
,它与只调用一次具有相同的效果。
[[unsequenced]]
此属性断言函数是一个 无序函数,这意味着
无状态意味着
static
或thread_local
局部变量不能是非const
,也不能是volatile
。
独立意味着函数的所有调用都会看到相同的全局变量值,不会改变全局状态,也不会通过指针参数改变任何状态。
to_upper_case
不是独立的,但像 strlen
这样的函数可以是独立的。
直观地说,未排序函数可以任意排序,甚至可以在其观察到的状态变化之间并行排序:(另请参阅标准中的脚注 196)
char *str = /* ... */; // A
strlen(str);
global = 123;
strlen(str);
strcpy(str, /* ... */); // B
在此示例中,点
strlen
和 A
之间可以有一次、两次或无限多次对 B
的调用。这些可以顺序发生,也可以并行发生。无论如何,“未排序”函数的结果必须相同。 global
的突变不允许改变strlen
的结果。GCC 属性注意事项
和
const
是这些标准属性的灵感来源,并且行为类似。请参阅N2956 5.8 与 GCC const 和 pure 的一些差异进行比较。简而言之:
pure
[[independent]]
更轻松
const
[[unsequenced]]
更严格
一般来说,应用它们时必须非常小心。该程序格式不正确,如果将它们应用于不具有断言属性的函数,则无需进行诊断。我们鼓励编译器检测这些属性的此类滥用,但这不是必需的。
常见示例(和惊喜)
printf
strlen
memcmp
可以是 [[unsequenced]]
memcpy
[[reproducible]]
memmove
幂等
fabs
[[unsequenced]]
sqrt
errno