Des*_*tor 22 c++ static-members nullptr language-lawyer c++11
最近尝试了以下程序,它编译,运行正常并产生预期的输出,而不是任何运行时错误.
#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;
}
Run Code Online (Sandbox Code Playgroud)
如果未初始化的指针用于访问类和/或结构成员,则行为未定义,但为什么允许使用空指针访问静态成员.我的计划有什么危害吗?
Col*_*mbo 28
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;
Run Code Online (Sandbox Code Playgroud)
在此语句中,*d是根据[stmt.expr]丢弃的值表达式.所以*d单独评估1,就像在d->a.
因此,如果*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]中的引用仍然存在,并且属于多态类类型的glvalues,在以下示例中就是这种情况:
int main() try {
// Polymorphic type
class A
{
virtual ~A(){}
};
typeid( *((A*)0) );
}
catch (std::bad_typeid)
{
std::cerr << "bad_exception\n";
}
Run Code Online (Sandbox Code Playgroud)
该程序的行为是明确定义的(将抛出异常并捕获),并且表达式*((A*)0) 被计算,因为它不是未评估操作数的一部分.现在如果间接通过空指针引起UB,那么表达式写成
*((A*)0);
Run Code Online (Sandbox Code Playgroud)
就是这样做,诱导UB,与typeid场景相比,这似乎是荒谬的.如果仅仅因为每个丢弃值表达式为1来评估上述表达式,那么在第二个片段UB中进行评估的关键区别在哪里?没有现有的实现分析typeid-operand,找到最里面的,相应的解引用并用一个检查包围它的操作数 - 也会有性能损失.
该问题中的注释随后结束了以下的简短讨论:
我们同意标准中的方法似乎没问题:
p = 0; *p;本质上不是错误.左值到右值的转换会给它带来未定义的行为.
即委员会同意这一点.虽然提出的所谓" 空左值 "的报告的决议从未被采纳过......
但是,"不可修改"是一个编译时概念,而实际上它处理运行时值,因此应该产生未定义的行为.此外,还有其他可以出现左值的上下文,例如左操作数.或.*,也应该受到限制.需要进一步起草.
...... 这不会影响理由.然后,应该注意的是,这个问题甚至先于C++ 03,这使得它在我们接近C++ 17时不那么有说服力.
CWG-issue #315似乎也涵盖了您的案例:
另一个要考虑的实例是从空指针调用成员函数:
Run Code Online (Sandbox Code Playgroud)struct A { void f () { } }; int main () { A* ap = 0; ap->f (); }[...]
理由(2003年10月):
我们同意应该允许这个例子.根据5.2.5 [expr.ref]
p->f()重写(*p).f().*p为pnull 时不是错误, 除非将左值转换为右值(4.1 [conv.lval]),它不在此处.
根据这个基本原理,通过空指针本身的间接不会在没有进一步的左值到右值转换(=访问存储值),引用绑定,值计算等的情况下调用UB.(Nota bene:使用空指针调用非静态成员函数应该调用UB,尽管只是被[class.mfct.non-static]/2勉强禁止.在这方面,基本原理已经过时了.)
仅仅是评估*d不足以调用UB.不需要对象的标识,也不是它先前存储的值.另一方面,例如
*p = 123;
Run Code Online (Sandbox Code Playgroud)
由于存在左操作数的值计算,因此未定义,[expr.ass]/1:
在所有情况下,在右和左操作数的值计算之后对赋值进行排序
因为左操作数应该是glvalue,所以glvalue引用的对象的标识必须按照[intro.execution]/12中表达式的评估定义所述来确定,这是不可能的(因此导致到UB).
1 [expr]/11:
在某些情况下,表达式仅出现其副作用.这种表达式称为丢弃值表达式.计算表达式并丢弃其值.[...].当且仅当表达式是volatile限定类型的glvalue且[...]时,才应用左值到右值转换(4.1).