c++ - 将 nullptr 分配给成员指针

谁能告诉我为什么分配给 Class::* 类型的数据成员指针的 nullptr 的内部表示对于 MSVC、clang 和 g++ 是 -1?对于 64 位系统,(size_t)1 << 63 是最好的,因为如果你使用 nullptr 成员指针,你肯定会触及内核内存并崩溃,所以这将是一个很好的调试帮助。

-1背后有更深层次的原因吗?

样本:

struct X
{
    int x, y;
};

using member_ptr = int X::*;

member_ptr f()
{
    return nullptr;
}

... 使用 g++ 生成以下二进制文件:

movq    $-1, %rax
ret

回答1

~(0LLU) 更可取的原因有以下三个:

  1. 成员指针可以是从 0 到结构或类大小的任何值。使用 ~(0LLU) 与实际有效的成员指针发生冲突的风险最小。您不能真正拥有 size_t 大小的结构:

    <source>:2:21: error: size '9223372036854775808' of array 'x' exceeds maximum object size '9223372036854775807'
        2 |     long long x[1LLU<<63];
    
    <source>:2:15: error: size of array 'x' exceeds maximum object size '9223372036854775807'
        2 |     long long x[1LLU<<62];

    请注意,限制是 (1LLU<<63) - 1。所以那种否定了这个论点。在 16 位系统上可能会有所不同。

  2. 在 x86_64 上加载 0~(1LLU)1LLU << 63 变为

    31 ff                            xor    %edi,%edi
    48 c7 c7 ff ff ff ff             mov    $0xffffffffffffffff,%rdi
    48 bf 00 00 00 00 00 00 00 80    movabs $0x8000000000000000,%rdi

    加载 0 是最快的。加载 1LLU << 63 是最长的操作码,仅此一项就有性能成本。所以使用 ~(0LLU) 作为成员指针 nullptr 有轻微的性能优势。

    在很多架构上都是类似的。在 Mips64 上,最后一个需要一个额外的操作码:https://godbolt.org/z/3nehjcoM6

  3. 从旧的 C 时代开始,函数返回 -1~(0LLU) 作为错误代码是一种习惯,但使用 0 的指针除外。成员指针不能使用 0。

我个人认为编译器开发人员只是遵循旧习惯(原因 3)。它也更快只是运气(或者那些老 C geezers 知道他们在哪里选择错误代码:)。

至于为什么编译器在优化时不能使用~(0LLU),在调试时不能使用1LLU << 63:你可以编译一些翻译单元作为优化代码和一些调试代码。然后,它们将遵循不兼容的 ABI,并且无法链接在一起。