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