c ++使用空指针访问静态成员

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似乎也涵盖了您的案例:

另一个要考虑的实例是从空指针调用成员函数:

  struct A { void f () { } };
  int main ()
  {
    A* ap = 0;
    ap->f ();
  }
Run Code Online (Sandbox Code Playgroud)

[...]

理由(2003年10月):

我们同意应该允许这个例子.根据5.2.5 [expr.ref] p->f()重写 (*p).f().*ppnull 时不是错误, 除非将左值转换为右值(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).

  • 看起来这个决议并没有成为任何标准.可能是因为它对参考文献意味着什么...... (4认同)
  • @Columbo Clang和GCC有一整套.https://gcc.gnu.org/onlinedocs/gcc/Debugging-Options.html,搜索`-fsanitize`. (3认同)
  • “如果没有进一步的左值到右值转换(=访问存储值)或引用绑定,取消引用空指针不会调用 UB”,这是一些人想要的,而不是实际情况。对我延长报价感到满意吗? (2认同)
  • @Columbo一个将明确定义的代码转换为错误的消毒器将使IMO变得非常烦人。无论如何,如果允许该标准仅允许“疯狂的”代码,我认为没有任何理由不对该标准进行限制。 (2认同)