【问题标题】:Why is this pointer null为什么这个指针为空
【发布时间】:2021-01-16 10:56:10
【问题描述】:

在 Visual Studio 中,指向成员变量的指针似乎是幕后的 32 位有符号整数(即使在 64 位模式下也是如此),并且在该上下文中空指针为 -1。因此,如果我有这样的课程:

#include <iostream>
#include <cstdint>

struct Foo
{
    char arr1[INT_MAX];
    char arr2[INT_MAX];
    char ch1;
    char ch2;
};


int main()
{
    auto p = &Foo::ch2;
    std::cout << (p?"Not null":"null") << '\n';
}

它编译并打印“null”。那么,是我造成了某种未定义的行为,还是编译器应该拒绝此代码而这是编译器中的错误?

编辑:

看来我可以保留“2 INT_MAX 数组加 2 个字符”模式,只有在这种情况下,编译器才允许我添加任意数量的成员,并且第二个字符始终被视为空值。 See demo。如果我稍微改变了模式(比如 1 或 3 个字符而不是 2 个),它会抱怨类太大。

【问题讨论】:

  • @EdoardoRosso 为什么你认为它有垃圾价值?我认为你应该阅读Pointer to class data member “::*”
  • @EdoardoRosso 不,你错了。 struct 一个类。
  • @EdoardoRosso 不,这不是垃圾。成员指针根本不是指针。它不需要对象存在。
  • @BillLynch 偏移量分别为 0、2147483647、4294967294、4294967295。这似乎是正确的。
  • IntelliSense 解析器正确识别问题,编译器没有。有点棘手,因为这个问题只能在后端检测到。这是 x64 代码生成器的限制,对象不能大于 2GB。除此之外,需要使用一种非常不同的方式来生成地址,由于位移溢出,LEA 不能再工作了。无法使用。使用帮助 > 发送反馈 > 报告问题

标签: c++ visual-studio language-lawyer


【解决方案1】:

根据标准 [1] 的附件 B,对象的大小限制由实现定义。你的结构大小太荒谬了。

如果结构是:

struct Foo
{
    char arr1[INT_MAX];
    //char arr2[INT_MAX];
    char ch1;
    char ch2;
};

...在相对较新的 64 位 MSVC 版本中,结构的大小似乎约为 2147483649 字节。如果你再添加 arr2,突然 sizeof 会告诉你 Foo 的大小为 1。

C++ 标准(附录 B)规定编译器必须记录限制,MSVC 就是这样做的 [2]。它声明它遵循建议的限制。附件 B,第 2.17 节为对象的大小提供了 262144(?) 的建议限制。虽然很明显 MSVC 可以处理更多,但它记录了它遵循最低建议,所以我认为当你的对象大小超过这个值时你应该小心。

[1]http://eel.is/c++draft/implimits

[2]https://docs.microsoft.com/en-us/cpp/cpp/compiler-limits?view=vs-2019

【讨论】:

  • 这大约是一个对象的大小,我们这里没有Foo 类型的对象。
  • @AyxanHaqverdili:严格来说,MSVC 对类型的大小有限制,这确实与对象大小的限制略有不同(尤其是在数组方面)。附录 B 允许这两种限制,但只建议对象的值。
  • 所以答案是我导致了未定义的行为,还是编译器应该拒绝此代码?
  • 当您超过上述限制时会发生什么是未定义的行为,因为标准没有定义当您超过该限制时会发生什么,sizeof 也没有定义在上面的示例中应该发生什么。然而,sizeof 结果不能为 0,因此值为 1。从可用性的角度来看,编译器并没有说 something 关于达到的限制,这有点荒谬。
  • @AyxanHaqverdili 虽然这里没有明确的 Foo 类型的对象,但当您执行“sizeof”之类的操作时,它通常处理给定类型的对象表示或相对于一种。例如,指向非静态类成员的指针被描述为“在给定类的对象中标识给定类型的成员”的东西。 eel.is/c++draft/basic#compound-1.8
【解决方案2】:

这显然是指针到成员表示的优化(在不存在虚拟基时仅使用 4 个字节的存储空间)与鸽巢原则之间的冲突。

对于包含N 类型char 的子对象的X 类型,有N+1 可能的char X::* 类型的有效成员指针...每个子对象一个,一个空值-指向成员的指针。

这在指向成员的表示中至少有 N+1 个不同的值时有效,这对于 4 字节表示意味着 N+1 32 并且因此最大对象大小为 232 - 1.

不幸的是,有问题的编译器使最大对象类型大小(在它拒绝程序之前)等于 232,这太大了并且会产生鸽巢问题——至少一对指向成员的指针必须是不可区分的。指向成员的空指针不一定是这一对的一半,但正如您在此实现中所观察到的那样。

【讨论】:

  • 好点。只要保持“两个 INT_MAX 数组 + 两个字符”模式,我就可以继续添加任意数量的成员,并且它将最后一个标记为空。 godbolt.org/z/q99786
  • @AyxanHaqverdili:哦,天哪,这仍然不会触发“类太大”错误?看起来那个特定的诊断完全被破坏了。
【解决方案3】:

表达式&amp;Foo::ch2 的类型为char Foo::*,它是指向类Foo 的成员的指针。根据规则,转换为 bool 的成员的指针只有在它是空指针时才应被评估为 false,即分配给它的 nullptr

这里的错误似乎是实现的缺陷。即在 gcc 编译器上,分配了 -march=x86-64 any 指向成员的指针评估为非 null (1),除非它使用以下代码分配了 nullptr:

struct foo
{
    char arr1[LLONG_MAX];
    char arr2[LLONG_MAX];
    char ch1;
    char ch2;
};

int main()
{
    char  foo::* p1 = &foo::ch1;
    char  foo::* p2 = &foo::ch2;
    std::cout << (p1?"Not null ":"null ") << '\n';
    std::cout << (p2?"Not null ":"null ") << '\n';
    
    std::cout << LLONG_MAX + LLONG_MAX << '\n';
    std::cout << ULLONG_MAX << '\n';
    std::cout << offsetof(foo, ch1) << '\n';
}

输出:

Not null 
null 
-2
18446744073709551615
18446744073709551614

这可能与类大小超出平台限制这一事实有关,导致成员偏移量为 0(nullptr 的内部值)。编译器没有检测到它,因为它成为... 带符号值的整数溢出的受害者,并且通过使用带符号的文字作为数组大小来导致 UB 在编译器内是程序员的错:LLONG_MAX + LLONG_MAX = -2将是两个数组组合的“大小”。

前两个成员的大小基本上计算为,ch1的偏移量为-2,表示为无符号18446744073709551614。 而-2 因此指针不为空。另一个编译器可能会将值钳制为 0 产生一个 nullptr,或者像 clang 那样实际检测存在的问题。

如果ch1 的偏移量是-2,那么ch2 的偏移量是-1?让我们添加这个:

std::cout << reinterpret_cast<signed long long&&> (offsetof(foo, ch1)) << '\n';
std::cout << reinterpret_cast<signed long long&&> (offsetof(foo, ch2)) << '\n';

附加输出:

-2
-1

第一个成员的偏移量显然是0,如果指针表示偏移量,那么它需要另一个值来表示nullptr。假设 this 特定编译器仅将 -1 视为空值是合乎逻辑的,这对于其他实现可能是也可能不是。

【讨论】:

  • 你的观点是 “在 gcc 编译器上,任何分配给成员的指针的计算结果都是非 null”:gcc 和 clang 在使用 -m32 编译时都返回 null:godbolt.org/z/znn5En
  • @BillLynch 这正在改变限制.. 但我正要补充一点,只需要查看 cde 和实验
  • 请注意offsetof(struct Foo, ch2) == 0xffffffff,而不是0
  • @BillLynch 无法使用 -m32 或 -march=x86-64 和 LLONG_MAX 重现您的结果,也许您有一些不常见的构建。尝试了 4.8 和 10\11。 0xffffffff 是-1,它是非nullptr,它是环绕的结果。带有 -m32 的 gcc 返回非 null,您可能使用了一些不方便的 g++ 构建或不同的运行时库(它也可能会影响它)。 clang 会正确检测到问题
  • 要使您的代码类似于 OP,您应该使用char foo::*p = &amp;foo::ch2,而不是ch1。修复后,这也会返回 "null"godbolt.org/z/Gc3eEo
【解决方案4】:

当我测试代码时,VS 显示Foo: the class is too large

当我添加char arr3[INT_MAX] 时,Visual Studio 将报告Error C2089 'Foo': 'struct' too large。 Microsoft Docs 将其解释为 The specified structure or union exceeds the 4GB limit.

【讨论】:

  • 在我的测试中,只有当我添加第三个带有 INT_MAX 元素的数组时才会发生该错误。其他 cmets 也验证了我使用 2 个数组观察到的行为。也许您没有进行干净的构建?我不确定。
  • 我已经清理并重建了我的编译器,这可能与每个人的编译器设置有关。我的意思是这可能是因为结构中的元素超出了struct的编译器限制。
  • 有趣的是,当你把char ch1;注释掉或者把INT_MAX改成INT_MAX-1,程序的结果就是NOT NULL
  • 这是因为该类经过精心编写以干净地环绕指向 -1 的指针。当你改变东西时,这不会发生。
  • use text, not images/links, for text--including tables & ERDs。仅将图像用于无法表达为文本或增强文本的内容。在图片中包含图例/键和说明。