有没有一种安全的方法来指定一个对象的值可能因为从未使用过而未初始化?

Is there a safe way to specify the value of an object may be uninitialized because it is never used?

免责声明:以下纯属学术问题;我将此代码与任何生产系统保持至少 100 m 的距离。这里提出的问题是无法在任何“现实生活”案例中衡量的。

考虑以下代码 (godbolt link):

#include <stdlib.h>

typedef int (*func_t)(int *ptr); // functions must conform to this interface

extern int uses_the_ptr(int *ptr);
extern int doesnt_use_the_ptr(int *ptr);

int foo() {
    // actual selection is complex, there are multiple functions,
    // but I know `func` will point to a function that doesn't use the argument
    func_t func = doesnt_use_the_ptr;

    int *unused_ptr_arg = NULL; // I pay a zeroing (e.g. `xor reg reg`) in every compiler
    int *unused_ptr_arg; // UB, gcc zeroes (thanks for saving me from myself, gcc), clang doesn't
    int *unused_ptr_arg __attribute__((__unused__)); // Neither zeroing, nor UB, this is what I want

    return (*func)(unused_ptr_arg);
}

编译器没有合理的方法知道 unused_ptr_arg 是不需要的(因此归零是浪费时间),但我知道,所以我想通知编译器 unused_ptr_arg 可能有任何值,例如用于将其传递给 func.

的寄存器中的任何值

有办法吗?我知道我超出了标准,所以我可以接受特定于编译器的扩展(尤其是 gcc 和 clang)。

int *unused_ptr_arg = NULL;

这是你应该做的。您无需支付任何费用。将 int 归零是空操作。好吧,技术上不是,但实际上是。你永远不会在你的程序中看到这个操作的时间。我并不是说它小到你不会注意到它。我的意思是它是如此之小,以至于许多其他因素和操作的数量级更长 "swallow" 它。

为了防止对 int foo() 的每个 运行 进行归零的可能性,只需使 unused_ptr_arg static.

int foo() {
    func_t func = doesnt_use_the_ptr;
    static int *unused_ptr_arg;    
    return (*func)(unused_ptr_arg);
}

使用GCC/Clang `asm` 构造

在 GCC 和 Clang 以及其他支持 GCC 扩展汇编语法的编译器中,您可以这样做:

int *unused_ptr_arg;
__asm__("" : "=x" (unused_ptr_arg));

return (*func)(unused_ptr_arg);

那个 __asm__ 构造 says “这里是一些汇编代码,此时要插入到程序中。它会将结果写入 unused_ptr_arg 您选择的任何位置。” (x 约束意味着编译器可以选择内存、处理器寄存器或机器支持的任何其他内容。)但实际的汇编代码是空的 ("")。所以没有生成汇编代码,但是编译器认为unused_ptr_arg已经初始化了。在用于 x86-64 的 Clang 6.0.0 和 GCC 7.3(目前在 Compiler Explorer 中的最新版本)中,这会生成一个没有 xor.

jmp

使用标准 C

考虑一下:

int *unused_ptr_arg;
(void) &unused_ptr_arg;

return (*func)(unused_ptr_arg);

(void) &unused_ptr_arg; 的目的是获取 unused_ptr_arg 的地址,即使该地址未被使用。这将禁用 C 2011 [N1570] 6.3.2.1 2 中的规则,该规则表示如果程序使用本可以用 register 声明的具有自动存储持续时间的未初始化对象的值,则行为未定义。因为它的地址被占用了,它不能用register声明,因此根据这个规则使用该值不再是未定义的行为。

因此,该对象具有不确定的值。然后是指针是否可能具有陷阱表示的问题。如果指针在所使用的 C 实现中没有陷阱表示,则不会因为仅引用该值而发生陷阱,就像将其作为参数传递时一样。

result with Clang 6.0.0 at Compiler Explorer是一条jmp指令,没有设置参数寄存器,即使在编译器选项中添加了-Wall -Werror。相反,如果删除 (void) 行,则会导致编译器错误。

这在所有架构中实际上是不可能的,这是有充分理由的。

对函数的调用可能需要将其参数溢出到堆栈,在 IA64 中,将未初始化的寄存器溢出到堆栈可能会崩溃,因为寄存器的先前内容是加载未映射地址的推测性加载。