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

Posted

技术标签:

【中文标题】为啥在使用静态方法时取消引用 nullptr 而不是 C++ 中的未定义行为?【英文标题】:Why is dereferencing of nullptr while using a static method not undefined behaviour in C++?为什么在使用静态方法时取消引用 nullptr 而不是 C++ 中的未定义行为? 【发布时间】:2020-11-29 13:28:42 【问题描述】:

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

考虑(来自上述帖子的简化示例):

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


A* pnullptr;

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

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

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

...*pp 为空时不是错误,除非左值被转换为右值(7.1 [conv.lval]),它不在这里。

从而区分/*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*/ 在 12.2.2 中明确指出为未定义 [class.mfct.non-static],尽管有人可能会争辩说,因为non_static_mem_fn(); 是 空,没有左值->右值转换。

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


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


附录:

this question asks about why dereferencing a nullptr is undefined behaviour。虽然我同意在大多数情况下这是一个坏主意,但根据此处的链接和引用,我不认为该陈述绝对正确。 类似的讨论 in this Q/A 带有一些指向第 232 期的链接。 我找不到专门针对静态方法和nullptr 取消引用问题的问题。也许我错过了一些明显的答案。

【问题讨论】:

评论不用于扩展讨论;这个对话是moved to chat。 这能回答你的问题吗? c++ access static members using null pointer 【参考方案1】:

此答案中的标准引用来自 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-&gt;E2 被转换为等价形式(*(E1)).E2 ...

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

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

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

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

// 1
p->static_mem_fn();

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

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

传统智慧会说“是”,但实际上并非如此。 标准中没有规定单独取消引用空指针是 UB,并且有几个讨论直接支持这一点:

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

围绕取消引用空指针的未定义行为存在核心问题。似乎意图是取消引用 定义良好,但使用取消引用的结果将产生未定义的行为。这个话题太混乱了,不能作为未定义行为的参考示例,或者如果要保留,应该更准确地说明。

此 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.

【讨论】:

很好的解释。对我来说,关键是两个代码块的等价性。这正是连接所有点的缺失部分。 @AntonMenshov 很高兴您发现它很有用!像往常一样,在回答晦涩难懂的 C++ 问题时,我在编写答案时也学到了一些东西。 可能有多种解释,这就是为什么要讨论使意图更明确的原因。 DR 1102 似乎支持空指针 deref 并不总是 UB 的解释,否则它不会作为 UB 的规范示例 被删除。请注意,[expr.unary.op]/1 也不以任何方式表明指向的对象必须与指针的类型匹配。这一段似乎在很多方面都没有得到充分说明,在某些情况下,行为被接受为定义。 在指针类型不匹配的情况下,这由 [basic.lval]/11 涵盖。本节专门讨论“访问对象的存储值”。这让我想知道这个代码是否是 UB:std::string foo; double *p = reinterpret_cast&lt;double *&gt;(&amp;foo); *p;。如果取消引用的结果未被使用,*p 是否被认为是“访问存储的值”?如果仅 deref 不足以被视为访问指针目标处的存储值,则这会让人怀疑空指针 deref 本身也是 UB 的说法。 @LanguageLawyer “结果是一个左值,指的是表达式所指向的对象或函数” 是......一个缺陷。为了 CWG 232 中的 typeid(*p)&amp;*p,委员会承诺(呵呵)允许取消引用空指针。它从未得到解决,但意图很明显。【参考方案2】:

常规成员函数有一个隐含的this-指针,而静态函数没有。调用静态成员函数时,不需要实例,只需要类型。

通常的语法是

A::static_mem_fn();

【讨论】:

这是一个用 language-lawyer 标记的问题。这是一种特殊的问题,其答案应基于标准提出论据。 我知道语言律师的问题。 OP已经引用了相关部分。

以上是关于为啥在使用静态方法时取消引用 nullptr 而不是 C++ 中的未定义行为?的主要内容,如果未能解决你的问题,请参考以下文章

Android - 为啥人们反复引用静态上下文内联,而不是在 Method() 中传递一次?

java中为啥要把main方法定义为一个static方法

如何在静态方法中取消分配使用std :: memory_resource分配的内存而不更改下面的函数签名

为啥 uint8_t 在分配给取消引用的 uint32_t 指针时使用了 4 个字节?

当空引用似乎不可能时,为啥我们会收到可能的取消引用空引用警告?

为啥代码会通过空指针显式调用静态方法?