为什么在使用静态方法时取消引用 nullptr 不是 C++ 中的未定义行为?

Why is dereferencing of nullptr while using a static method not undefined behaviour in C++?

我正在阅读 a post on some nullptr peculiarities in C++,一个特定的例子在我的理解中造成了一些混乱。

考虑(上述post的简化示例):

struct A {   
    void non_static_mem_fn() {}  
    static void static_mem_fn() {}  
};


A* p{nullptr};

/*1*/ *p;
/*6*/ p->non_static_mem_fn();
/*7*/ p->static_mem_fn();

根据作者的说法,取消引用 nullptr 的表达式 /*1*/ 本身不会导致未定义的行为。与使用 nullptr 对象调用静态函数的表达式 /*7*/ 相同。

理由是基于 issue 315 in C++ Standard Core Language Closed Issues, Revision 100

...*p is not an error when p is null unless the lvalue is converted to an rvalue (7.1 [conv.lval]), which it isn't here.

从而区分/*6*//*7*/

因此,nullptr 的实际取消引用不是未定义的行为answer on SO, discussion under issue 232 of C++ Standard,...)。因此,/*1*/的有效性在这个假设下是可以理解的。

可是,/*7*/如何保证不引起UB呢?根据引用的引述,p->static_mem_fn(); 中没有左值到右值的转换。但是 /*6*/ p->non_static_mem_fn(); 也是如此,我认为我的猜测已被同一期 315 中关于以下内容的引用所证实:

/*6*/ is explicitly noted as undefined in 12.2.2 [class.mfct.non-static], even though one could argue that since non_static_mem_fn(); is empty, there is no lvalue->rvalue conversion.

(在引用中,我更改了“which”和 f() 以获得与此问题中使用的符号的联系)。


那么,为什么在UB的因果关系上,对p->static_mem_fn();p->non_static_mem_fn();进行了这样的区分呢?是否有从可能是 nullptr 的指针调用静态函数的预期用途?


附录:

此答案中的标准引用来自 C++17 规范 (N4713)。

您的问题中引用的部分之一回答了 non-static 成员函数的问题。 [class.mfct.non-static]/2:

If a non-static member function of a class X is called for an object that is not of type X, or of a type derived from X, the behavior is undefined.

例如,这适用于通过不同的指针类型访问对象:

std::string foo;

A *ptr = reinterpret_cast<A *>(&foo); // not UB by itself
ptr->non_static_mem_fn();             // UB by [class.mfct.non-static]/2

空指针不指向任何有效对象,因此它当然也不指向A类型的对象。使用您自己的示例:

p->non_static_mem_fn(); // UB by [class.mfct.non-static]/2

既然如此,为什么这在静态情况下有效?让我们将标准的两个部分放在一起:

[expr.ref]/2:

... The expression E1->E2 is converted to the equivalent form (*(E1)).E2 ...

[class.static]/1(强调我的):

... A static member may be referred to using the class member access syntax, in which case the object expression is evaluated.

特别是第二个块说,即使对于静态成员访问,也会评估对象表达式。这一点很重要,例如,它是一个有副作用的函数调用。

放在一起,这意味着这两个块是等价的:

// 1
p->static_mem_fn();

// 2
*p;
A::static_mem_fn();

所以最后一个要回答的问题是当p是一个空指针值时*p单独是否是未定义的行为。

传统智慧会说“是”,但事实并非如此。 标准中没有任何内容表明单独取消引用空指针是 UB 并且有几个直接支持这一点的讨论:

  • Issue 315,正如您在问题中提到的,明确指出当结果未使用时 *p 不是 UB。
  • DR 1102 删除了作为 UB 示例的“解引用空指针”。给出的理由是:

    There are core issues surrounding the undefined behavior of dereferencing a null pointer. It appears the intent is that dereferencing is well defined, but using the result of the dereference will yield undefined behavior. This topic is too confused to be the reference example of undefined behavior, or should be stated more precisely if it is to be retained.

  • 此 DR 链接到 issue 232,其中讨论了在 p 为空指针时添加明确指示 *p 为已定义行为的措辞,只要结果不是用过。

总结:

p->non_static_mem_fn(); // UB by [class.mfct.non-static]/2
p->static_mem_fn();     // Defined behavior per issue 232 and 315.