为啥在使用静态方法时取消引用 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 有
...
*p
在p
为空时不是错误,除非左值被转换为右值(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 anullptr
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->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<double *>(&foo); *p;
。如果取消引用的结果未被使用,*p
是否被认为是“访问存储的值”?如果仅 deref 不足以被视为访问指针目标处的存储值,则这会让人怀疑空指针 deref 本身也是 UB 的说法。
@LanguageLawyer “结果是一个左值,指的是表达式所指向的对象或函数” 是......一个缺陷。为了 CWG 232 中的 typeid(*p)
和 &*p
,委员会承诺(呵呵)允许取消引用空指针。它从未得到解决,但意图很明显。【参考方案2】:
常规成员函数有一个隐含的this
-指针,而静态函数没有。调用静态成员函数时,不需要实例,只需要类型。
通常的语法是
A::static_mem_fn();
【讨论】:
这是一个用 language-lawyer 标记的问题。这是一种特殊的问题,其答案应基于标准提出论据。 我知道语言律师的问题。 OP已经引用了相关部分。以上是关于为啥在使用静态方法时取消引用 nullptr 而不是 C++ 中的未定义行为?的主要内容,如果未能解决你的问题,请参考以下文章
Android - 为啥人们反复引用静态上下文内联,而不是在 Method() 中传递一次?
如何在静态方法中取消分配使用std :: memory_resource分配的内存而不更改下面的函数签名
为啥 uint8_t 在分配给取消引用的 uint32_t 指针时使用了 4 个字节?