在C++20中,解引用空指针是否属于未定义行为?

4
我从研究问题“在C中,&((T*)NULL)->member是未定义行为吗?”开始。这是我的教科书中的一个例子,介绍了旧版的offsetof实现。
我知道根据cppreference页面,C++现在无法实现offsetof
但是在阅读一些C++ CWS问题后,我的问题变成了“解引用空指针是否是未定义行为”。
此外,我认为他们不会没有任何理由地改变offsetof的实现方式,即&((T*)NULL)->member,但我不知道原因,也许是因为它是未定义行为?但我没有找到一个术语说&((T*)NULL)->member在C中是未定义行为。对于C++来说,如果它不是标准布局类型,我认为它是未定义行为。
一开始,我以为会有一个明确指定“解引用空指针是未定义行为”的术语。
然而,随着我深入了解,我发现这比我想象的要复杂得多。
在阅读了很多stackoverflow文章回复后,我发现答案并不统一。
有些帖子说它是明确定义的,有些帖子说它是未定义行为,还有些帖子说它是非指定的。
对于那些认为这是明确定义的帖子,他们引用了“CWG issue #232”和“CWG issue #315”作为理由,就像在c++ access static members using null pointer中的答案一样。
对于那些认为这是未指定的帖子,他们说标准中没有明确指定。
对于那些认为这是未定义行为的帖子,他们说该问题尚未包含在标准中,因此仍然是未定义行为。此外,他们提到了“如果将无效值分配给指针,则一元运算符*的行为是未定义的。”这个术语。
上述stackoverflow中的示例为:
#include <iostream>
class demo {
public:
  static void fun()
  {
    std::cout << "fun() is called\n";
  }
  static int a;
};

int demo::a = 9;

int main()
{
  demo *d = nullptr;
  d->fun();
  std::cout << d->a;
  return 0;
}

他们说这是明确的原因大致如下:
  1. E1->E2 等同于 (*(E1)).E2
  2. 因此,如果 *d; 是合法的,那么 d->fun() 也是合法的。
  3. CWG issue #232 表示 p = 0; *p; 并不是本质上的错误。左值到右值的转换会导致其行为未定义。
  4. CWG issue #315 表示在上述示例中,当 d 为 null 时,*d 不是错误,除非将左值转换为右值(7.3.2 [conv.lval]),但在这里并没有进行转换。
  5. 因此,*d; 是合法的,那么 d->fun() 也是合法的。

这个问题在大约2005年左右讨论过,当时还是在C++03规范中。

然而,在C++20中,对于->,标准明确指定了E1E1->E2中应该是prvalue:

n4861(expr.ref#2):对于第二种选项(箭头),第一个表达式必须是具有指针类型的prvalue。

所以我认为在这里可能会有一个lvalue-to-rvalue转换,因为E1应该是prvalue。

顺便说一下,在之前的标准中,"dereference null pointer"被用作未定义行为的例子。

n1146(intro.execution#4):这个国际标准中描述了某些其他操作为未定义行为(例如,解引用空指针的效果)。

但是这个例子在CWG issue #1102中被修改了。他们给出的原因是

對於解引用空指針的未定義行為存在著核心問題。看起來意圖是解引用明確定義的,但使用解引用結果會產生未定義行為。這個話題太混亂了,不適合作為未定義行為的參考例子,或者如果要保留的話應該陳述得更明確。
這個問題在2010年被討論過,已經過去了13年了,所以我認為這個問題已經存在很長時間了,但可惜的是,我現在還是找不到答案。
總而言之,是否可以由一位語言專家給我一個關於這個問題的結論?在C++20中,解引用空指針是未定義行為嗎?例如,&((T*)NULL)->member和上面的d->fun()。或者是未指定行為還是未規定行為?
希望能提供相關的歷史和標準術語。

编辑:
我的总结是这仍然是一个未解决的问题,目前它总是通过省略UB(未定义行为)来进行操作,expr.unary#op-1.sentence-3只在指针指向对象时才定义了行为。但这可能不是预期的规范。

顺便说一下,有一个最新的讨论涉及到相同的结果:https://github.com/cplusplus/CWG/issues/198

请查看@user17732522的评论和@Brian Bi的回答


4
请只使用一种语言,要么是C++,要么是C(它们不是同一种语言)。在C++中,这很可能永远都是未定义行为。而且&((T*)NULL)->member是你在C++中永远不会写的东西,即使没有空指针。最好的情况是std::dynamic_cast<T*>(&ptr)(即使使用dynamic_cast通常也意味着设计上的缺陷)。你能解释一下为什么解引用nullptr是个问题吗? - Pepijn Kramer
3
你基本上涵盖了关于这个问题的所有内容。CWG 232仍然没有得到解决。 - user17732522
2
"所以我认为这里可能存在一个lvalue到rvalue的转换,因为E1应该是prvalue吗?": 这与是否会出现未定义行为无关。指针值本身作为空指针是完全有效的。问题是间接引用产生的lvalue是否可以是"null lvalue",目前还没有明确规定。因此,严格的答案是根据https://eel.is/c++draft/expr.unary#op-1.sentence-3中的遗漏情况,它总是未定义行为,该规范只在指针指向对象时定义了行为。但这可能不是预期的规范。 - user17732522
1
nullptr是C++中空指针的标准值。NULL只是一个#define 0,因此不具备类型安全性。 - Pepijn Kramer
2
在C++98和C++20中,d->fun()(*d).fun()是等价的,并且在两者中都对d进行了左值到右值的转换,但没有对*d进行转换。问题是在*d上假设了一个左值到右值的转换,例如,如果d的类型是int*,则可以写成*d + 1 - user17732522
显示剩余24条评论
1个回答

4
截至目前,关于解引用空指针是否是未定义行为的问题仍未解决。而且目前还不清楚CWG 232中所指示的方向,即只有在尝试通过解引用的结果访问值时才会是未定义行为,是否仍然是CWG的共识(尽管至少有一种情况下是明确合法的,即当结果的左值是多态类型并且是typeid的操作数时)。如果CWG能就一个方向达成一致意见,那么也不清楚EWG是否会接受这个方向。所以,实际上没有人知道答案。
至少有一个很好的理由说明为什么&((T*)NULL)->member应该是未定义行为。一个实现可能通过将固定偏移量添加到E的值来计算&E->m。如果E是一个空指针,这种算术运算将生成一个地址值,硬件可能会识别为无效地址,在某些实现中,将无效指针值加载到寄存器中会导致陷阱。我想,如果CWG 232最终得到解决,它可能会澄清这种情况是未定义行为。

抱歉给你带来了误导,我的意思是,我以为如果我们说一个行为是未定义的,那就意味着标准中有一个或多个条款明确或隐含地指定了该行为是未定义的。因此,尽管我可以理解“如果E是一个空指针,对其添加偏移量是无效的”,但如果我要将这个问题归类为未定义行为,我认为我需要找到一个术语来证明它,否则,我只能将其归类为你所说的“未解决”或“奇怪的事情”。可悲的是,我已经寻找了几天,但仍然找不到这些术语,除了@user17732522上面给我的那个术语。 - Mes
1
正如我在回答中所说的,指针算术是导致它应该是未定义行为(UB)的原因,也是我预期它最终会被明确为UB的原因。但正如你所说,目前还没有实际的规范说明它是UB。 - Brian Bi
啊,难怪我在标准中找不到它。谢谢你的分类 :) 我想确认一下,&((T*)NULL)->member 中没有任何解引用吗?就像上面的注释说的那样,这只是一个偏移计算? - Mes
2
如果 m 不是 T 的虚拟基类,那么 &(p->m) 通常应该编译成一个简单的加法指令,其中 p 是指向 T 的指针。如果 m 在一个虚拟基类中,那么可能需要读取内存来计算 m 的偏移量。 - Brian Bi
1
@Mes 提问无关的问题的地方不是评论区。 - Brian Bi
显示剩余3条评论

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