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] 丢弃的值表达式。So*d
仅评估1,就像在 中一样d->a
。
因此 if*d;
是有效的,或者换句话说,表达式的评估,*d
你的例子也是如此。
通过空指针进行间接寻址是否会固有地导致未定义的行为?
十五年前创建的开放 CWG 问题#232涉及这个确切的问题。提出了一个非常重要的论点。报告开始于
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,那么在第二个片段 UB 中进行评估的关键区别在哪里?没有现有的实现可以分析typeid
- 操作数,找到最里面的相应解引用并用检查包围它的操作数 - 也会有性能损失。
然后,该问题中的注释结束了简短的讨论:
我们同意标准中的方法似乎还可以:p = 0; *p;
本质上不是错误。左值到右值的转换会给它未定义的行为。
即委员会同意这一点。尽管本报告提出的引入所谓“空左值”的决议从未被采纳……</p>
然而,“不可修改”是一个编译时概念,而实际上它处理的是运行时值,因此应该产生未定义的行为。此外,还有其他可能出现左值的上下文,例如 的左操作数。或 .*,这也应该受到限制。需要额外起草。
…<strong>这不影响基本原理。话又说回来,应该注意的是,这个问题甚至早于 C++03,这使得我们在接近 C++17 时不太令人信服。
CWG-issue #315似乎也涵盖了您的情况:
另一个需要考虑的例子是从空指针调用成员函数:
struct A { void f () { } };
int main ()
{
A* ap = 0;
ap->f ();
}
[…]
理由(2003 年 10 月):
我们同意应该允许这个例子。根据 5.2.5 [expr.ref]p->f()
重写
。当为 null 时不是错误,
除非左值被转换为右值(4.1 [conv.lval]),它不在这里。(*p).f()
*p
p
根据这个原理,如果没有进一步的左值到右值转换(=访问存储的值)、引用绑定、值计算等,通过空指针本身的间接调用不会调用 UB。(注意事项:使用空指针调用非静态成员函数应该调用 UB,尽管 [class.mfct.non-static]/2 只是模糊地不允许这样做。在这方面,基本原理已经过时了。)
即仅仅评估*d
不足以调用UB。不需要对象的身份,也不需要其先前存储的值。另一方面,例如
*p = 123;
未定义,因为存在对左操作数 [expr.ass]/1 的值计算:
在所有情况下,赋值都是在左右操作数的值计算之后排序的
因为左操作数应该是一个glvalue,所以该glvalue引用的对象的身份必须由[intro.execution] / 12中表达式的评估定义中提到的那样确定,这是不可能的(因此导致到 UB)。
1 [expr] / 11:
在某些情况下,表达式只出现在它的副作用上。这样的表达式称为弃值表达式。计算表达式并丢弃其值。[…]。当且仅当表达式是 volatile 限定类型的左值并且 […]