在 C++ 中,使用静态方法时对空指针进行解引用为什么不是未定义的行为?

13
我正在阅读关于C++中nullptr的一些特殊性质的帖子,其中一个例子让我有些困惑。
考虑以下内容(来自上述帖子的简化示例):
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();

根据作者的说法,解引用空指针的表达式/*1*/本身不会导致未定义行为。同样,使用空指针对象调用静态函数的表达式/*7*/也是如此。其理由基于C++标准核心语言已关闭问题中的issue 315,其中有以下描述:"当p为空时,除非该左值被转换为右值(7.1 [conv.lval]),否则*p不是错误",从而区分了/*6*//*7*/。因此,实际上对空指针进行解引用不属于未定义行为在SO上的回答, 在C++标准的232号问题下的讨论等)。因此,在这种假设下,/*1*/的有效性是可以理解的。
然而,如何保证/*7*/不会导致未定义行为呢?根据引用的语句,在p->static_mem_fn();中没有将左值转换为右值。但是对于/*6*/p->non_static_mem_fn();也是如此。我认为,在相同的问题315中,以下描述证实了我的猜测:"即使non_static_mem_fn();为空,/*6*/在12.2.2 [class.mfct.non-static]中明确说明是未定义的"(在引用中,我更改了"which"和f()以获取与本问题中使用的符号的联系)。
那么,为什么在处理UB的因果关系时要对

p->static_mem_fn();

p->non_static_mem_fn();

进行区分?是否有故意从可能是nullptr的指针调用静态函数的使用场景?

附录:

  • 这个问题询问为什么解引用nullptr是未定义的行为。虽然我同意在大多数情况下这是一个坏主意,但我不认为这个声明在这里的链接和引用中是绝对正确的。
  • 类似的讨论在这个问答中,其中包含一些与232号问题有关的链接。
  • 我没有找到一个专门探讨静态方法和nullptr解引用问题的问题。也许我错过了一些明显的答案。

评论不适合进行长时间的讨论;此对话已被移至聊天室 - Machavity
这个回答解决了你的问题吗?c++如何使用空指针访问静态成员变量? - Passer By
1个回答

6

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

你问题中引用的一个章节回答了非静态成员函数的问题。[class.mfct.non-static]/2:

如果对于一个不属于类型X或X的派生类类型的对象调用类X的非静态成员函数,则其行为是未定义的。

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

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:

... 表达式 E1->E2 被转换成等价形式 (*(E1)).E2 ...

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

... 静态成员可以使用类成员访问语法进行引用,此时将对对象表达式求值。

特别地,第二个部分表示即使是静态成员访问,也会对对象表达式进行求值。如果例如是具有副作用的函数调用,则这一点很重要。

综上所述,这意味着这两个部分是等价的:

// 1
p->static_mem_fn();

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

那么最终需要回答的问题是,当 p 是空指针值时,仅仅对 *p 进行解引用是否会产生未定义行为。
传统观点认为“是”,但实际上这并不正确。标准中没有规定仅仅解引用空指针就是未定义行为,而且有一些讨论直接支持这个观点:
- 你在问题中提到的 Issue 315 明确说明了如果结果没有被使用,则 *p 不是未定义行为。 - DR 1102 将“解引用空指针”从未定义行为的例子中删除。其给出的理由是:

解引用空指针的未定义行为存在核心问题。看起来意图是解引用是被定义的,但使用解引用的结果会导致未定义行为。这个话题太混乱了,不能作为未定义行为的参考例子,或者应该更加精确地表述。

- 这个 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.

1
很好的解释。对我来说,关键是两个代码块的等价性。这正是连接所有点的缺失之处。 - Anton Menshov
1
@AntonMenshov 很高兴你觉得它有用!通常在回答晦涩的C++问题时,我在撰写答案时也学到了很多东西。 - cdhowie
1
有多种可能的解释,这就是为什么需要讨论使意图更明确。DR 1102似乎支持这样的解释,即空指针解引用并不总是UB,否则它不会被作为UB的典型示例而被删除。请注意,[expr.unary.op]/1没有以任何方式指示指向的对象必须与指针的类型匹配。这段话在多个方面都存在不足之处,在某些情况下,行为被认为是已定义的。 - cdhowie
1
在指针类型不匹配的情况下,[basic.lval]/11已经涵盖了此问题。该部分具体讨论了“访问对象的存储值”。这使我想知道这段代码是否是未定义行为:std::string foo; double *p = reinterpret_cast<double *>(&foo); *p;。如果解引用的结果未使用,*p是否被认为是“访问存储值”?如果仅仅进行解引用不足以被认为是访问指针目标处的存储值,那么对于空指针解引用的单独声明也会产生疑问,认为它是未定义行为。 - cdhowie
1
“@LanguageLawyer “结果是一个lvalue,引用表达式指向的对象或函数” 是一个缺陷。委员会致力于允许空指针解引用,以便在CWG 232中使用typeid(*p)&*p。尽管从未解决此问题,但意图是明显的。” - Passer By
显示剩余9条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接