TL;DR: 您的示例是定义良好的。仅对空指针取消引用不会调用UB。
关于这个主题存在很多争论,基本上归结为通过空指针进行间接寻址本身是否属于UB。
您的示例中唯一有问题的事情是对象表达式的评估。特别地,根据[expr.ref]/2:d->a
等同于(*d).a
:
将表达式E1->E2
转换为等效形式(*(E1)).E2
; 5.2.5之后的部分将仅处理第一个选项(点)。
*d
只是被评估:
点或箭头之前的后缀表达式被评估; 65该评估的结果连同id-expression决定整个后缀表达式的结果。
65) 如果评估类成员访问表达式,则即使结果不必要以确定整个后缀表达式的值,例如如果id-expression表示静态成员,子表达式评估也会发生。
让我们提取代码的关键部分。考虑表达式语句
*d;
在这个语句中,根据[stmt.expr],
*d
是一个被丢弃的值表达式。因此,
*d
仅会被求值一次,就像
d->a
一样。
因此,如果
*d;
是有效的,或者换句话说,表达式
*d
的求值是有效的,则你的示例也是有效的。
通过空指针进行间接引用本质上会导致未定义的行为吗?
已经有一个开放的CWG问题#232超过十五年,涉及到了这个确切的问题。提出了一个非常重要的论点。报告开始于
至少在IS的一些地方,规定通过null指针间接引用会产生未定义的行为:1.9 [intro.execution]第4段以“取消引用null指针”作为未定义行为的例子,而8.3.2 [dcl.ref]第4段则使用这种被认为是未定义行为来证明“null引用”的不存在。
请注意,所提到的示例已更改为覆盖对const
对象的修改,而[dcl.ref]中的注释 - 虽然仍存在 - 不是规范性的。规范性的条款已被删除以避免承诺。
然而,描述一元"*
"运算符的5.3.1 [expr.unary.op]第1段,并没有说如果操作数是null指针则行为未定义,这可能出人意料。此外,至少有一个段落给出了取消引用空指针的明确定义行为:5.2.8 [expr.typeid] 第2段说
如果lvalue表达式是通过将一元*运算符应用于指针获得的,并且指针是null指针值(4.10 [conv.ptr]),则typeid表达式引发bad_typeid异常(18.7.3 [bad.typeid])。
这是不一致的,应该进行清理。
最后一个观点尤其重要。[expr.typeid]中的引用仍然存在,并适用于多态类类型的glvalues,在以下示例中就是这种情况:
int main() try {
class A
{
virtual ~A(){}
};
typeid( *((A*)0) );
}
catch (std::bad_typeid)
{
std::cerr << "bad_exception\n";
}
这个程序的行为是明确定义的(将抛出并捕获异常),
表达式*((A*)0)
会被评估,因为它不是未评估操作数的一部分。现在,如果通过空指针进行间接引用导致UB,则写成以下表达式。
*((A*)0);
与typeid
情况相比,仅仅进行抛弃值表达式的计算,这时如果在第二段代码片段中进行评估的关键区别是什么,导致产生未定义行为?已经存在的实现不能分析typeid
操作数,找到最内层对应的解引用并用检查语句包围其操作数 - 这也会带来性能损失。
然后,该问题的一个注释以以下方式结束了简短的讨论:
我们一致认为标准中的方法是正确的:p = 0; *p;
本质上不是错误的。将lvalue转换为rvalue将产生未定义行为。
也就是说,委员会对此达成了一致意见。尽管这份报告提出的解决方案——所谓的“空lvalue”——从未得到采纳......
不过,“不可修改”是一个编译时概念,而实际上这涉及运行时值,因此应当产生未定义行为。此外,在其他上下文中也可能出现lvalue,例如 . 或 .* 的左操作数,也应受到限制。需要进行额外的起草。
......这不影响基本原理。然而,需要注意的是,这个问题甚至在C++03之前就存在了,这在我们接近C++17时可能不太令人信服。
CWG问题#315似乎也涵盖了您的情况:
另一个要考虑的实例是从空指针调用成员函数:
struct A { void f () { } };
int main ()
{
A* ap = 0;
ap->f ();
}
根据这个解释,仅仅通过空指针进行间接引用本身不会触发未定义的行为,除非进行了进一步的左值到右值转换(即访问存储的值),引用绑定、值计算或类似操作。(须知:使用空指针调用一个非静态成员函数应该会触发未定义的行为,尽管这在 [class.mfct.non-static]/2 中模糊地被禁止。在这方面,这个解释已经过时。)换句话说,仅仅评估 *d
不足以触发未定义的行为。不需要知道对象的身份,也不需要先前存储的值。另一方面,例如:*p = 123
由于左操作数和右操作数的计算结果顺序已确定,根据[expr.ass]/1:
在所有情况下,赋值操作在右操作数和左操作数的值的计算之后进行。
由于左操作数应为glvalue(广义左值),根据[intro.execution]/12中表达式求值的定义,必须明确确定glvalue所引用的对象的标识符,但这是不可能的(从而导致未定义行为)。
1 根据[expr]/11:
在某些上下文中,表达式仅出现以产生副作用。这种表达式称为“放弃值表达式”。计算表达式并丢弃其值。 如果表达式是volatile限定类型的glvalue,则应用lvalue-to-rvalue转换(4.1),条件是[...]