何时在null实例上调用成员函数会导致未定义的行为?

GMa*_*ckG 115 c++ standards-compliance null-pointer undefined-behavior language-lawyer

请考虑以下代码:

#include <iostream>

struct foo
{
    // (a):
    void bar() { std::cout << "gman was here" << std::endl; }

    // (b):
    void baz() { x = 5; }

    int x;
};

int main()
{
    foo* f = 0;

    f->bar(); // (a)
    f->baz(); // (b)
}
Run Code Online (Sandbox Code Playgroud)

我们期望(b)崩溃,因为x空指针没有相应的成员.在实践中,(a)不会崩溃,因为this从不使用指针.

因为(b)取消引用this指针((*this).x = 5;),并且this为null,程序进入未定义的行为,因为取消引用null总是被称为未定义的行为.

(a)导致未定义的行为吗?如果两个函数(和x)都是静态的呢?

GMa*_*ckG 112

双方(a)(b)导致不确定的行为.通过空指针调用成员函数始终是未定义的行为.如果该函数是静态的,那么它在技术上也是未定义的,但存在一些争议.


首先要理解的是为什么取消引用空指针是未定义的行为.在C++ 03中,这里实际上有点含糊不清.

虽然在§1.9/ 4和§8.3.2/ 4的注释中提到了"解除引用空指针导致未定义的行为",但它从未明确说明.(注释是非规范性的.)

但是,可以尝试从§3.10/ 2推导出它:

左值是指对象或函数.

解除引用时,结果是左值.空指针引用对象,因此当我们使用左值时,我们有未定义的行为.问题是前一句从未说过,所以"使用"左值是什么意思?甚至可以生成它,或者在更正式的执行左值到右值转换的意义上使用它?

无论如何,它绝对不能转换为右值(§4.1/ 1):

如果左值引用的对象不是类型T的对象,并且不是从T派生的类型的对象,或者如果对象未初始化,则需要此转换的程序具有未定义的行为.

这绝对是未定义的行为.

歧义来自于它是否是未定义的行为,而不是使用无效指针中的值(即获取左值而不是将其转换为右值).如果没有,那么int *i = 0; *i; &(*i);定义明确.这是一个活跃的问题.

所以我们有一个严格的"取消引用空指针,获取未定义的行为"视图和弱"使用解除引用的空指针,获取未定义的行为"视图.

现在我们考虑这个问题.


是的,(a)导致未定义的行为.实际上,如果this为null,则无论函数内容如何,结果都是未定义的.

这遵循§5.2.5/ 3:

如果E1类型为"指向类X的指针",则表达式E1->E2将转换为等效形式(*(E1)).E2;

*(E1)将导致具有严格解释的未定义行为,.E2并将其转换为右值,使其成为弱解释的未定义行为.

它也遵循直接来自(§9.3.1/ 1)的未定义行为:

如果为非X类型的对象或从X派生的类型调用类X的非静态成员函数,则行为未定义.


对于静态函数,严格与弱的解释会产生差异.严格来说,它是未定义的:

可以使用类成员访问语法来引用静态成员,在这种情况下,评估对象表达式.

也就是说,它被评估为非静态的,我们再一次取消引用空指针(*(E1)).E2.

但是,因为E1没有在静态成员函数调用中使用,如果我们使用弱解释,则调用是明确定义的.*(E1)导致左值,静态函数被解析,*(E1)被丢弃,函数被调用.没有左值到右值的转换,因此没有未定义的行为.

在C++ 0x中,从n3126开始,模糊性仍然存在.现在,安全:使用严格的解释.

  • +1.继续迂腐,在"弱定义"下,非静态成员函数未被称为"对于非X类型的对象".它被称为左值,它根本不是一个对象.因此,建议的解决方案会在您引用的子句中添加文本"或者如果左值是一个空的左值". (5认同)
  • 我不认为CWG缺陷315是"封闭的",因为它在"封闭问题"页面上的存在暗示.基本原理说应该允许它,因为当`p`为null时"`*p`不是错误,除非将左值转换为右值." 然而,这依赖于"空左值"的概念,这是[CWG缺陷232]的建议解决方案的一部分(http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active. html#232),但尚未被采纳.因此,使用C++ 03和C++ 0x中的语言,即使没有左值到右值的转换,取消引用空指针仍然是未定义的. (4认同)
  • “.E2 将其转换为右值,” - 呃,不,它不会 (2认同)

Mar*_*som 29

显然未定义意味着它没有被定义,但有时它可以是可预测的.我要提供的信息永远不应该依赖于工作代码,因为它当然不能保证,但在调试时可能会有用.

您可能认为在对象指针上调用函数将取消引用指针并导致UB.在如果函数不是虚拟实践,编译器将其转换为纯函数调用传递指针作为第一个参数这个,绕过解引用和用于被叫的成员函数创建定时炸弹.如果成员函数没有引用任何成员变量或虚函数,它实际上可能成功而没有错误.请记住,成功属于"未定义"的宇宙!

微软的MFC功能GetSafeHwnd实际上依赖于这种行为.我不知道他们在吸烟.

如果你正在调用一个虚函数,必须取消引用指针才能到达vtable,并且肯定你会得到UB(可能是崩溃但记住没有保证).

  • GetSafeHwnd 首先进行 !this 检查,如果为真,则返回 NULL。然后它开始一个 SEH 帧并取消对指针的引用。如果存在内存访问冲突 (0xc0000005),则会被捕获,并将 NULL 返回给调用者 :) 否则返回 HWND。 (2认同)
  • @ПетърПетров 我已经好几年没有查看“GetSafeHwnd”的代码了,从那时起他们可能已经增强了它。并且不要忘记他们拥有编译器工作原理的内部知识! (2认同)