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

时间:2015-02-12 16:34:29

标签: c++ c++11 language-lawyer static-members nullptr

最近尝试了以下程序并且它编译,运行正常并产生预期的输出而不是任何运行时错误。

#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;
}

如果未初始化的指针用于访问类和/或结构成员,则行为未定义,但为什么允许使用空指针访问静态成员。我的计划有什么危害吗?

5 个答案:

答案 0 :(得分:28)

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;

在此语句中,*d是根据[stmt.expr]丢弃的值表达式。所以*d仅被评估为 1 ,就像在d->a中一样 因此,如果*d;有效,或者换句话说评估表达式*d,那么您的示例也是如此。

通过空指针的间接是否会导致未定义的行为?

有一个开放的CWG问题#232,创建于十五年前,涉及这个确切的问题。提出了一个非常重要的论点。报告以

开头
  

IS中至少有几个地方表示间接通过   null指针产生未定义的行为:1.9 [intro.execution]   第4段给出&#34;解除引用空指针&#34;作为一个例子   未定义的行为,以及8.3.2 [dcl.ref]第4段(在注释中)使用   这种被认为是未定义的行为作为理由   不存在&#34;空引用。&#34;

请注意,上述示例已更改为涵盖const个对象的修改,[dcl.ref]中的注释 - 虽然仍然存在 - 但不是规范性的。删除了规范性段落以避免承诺。

  

然而,5.3.1 [expr.unary.op]第1段,其中描述了一元   &#34; *&#34;运算符,并没有说如果行为是未定义的   人们可能期望操作数是一个空指针。此外,至少   一个段落提供解除引用空指针明确定义的行为:   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";
}

此程序的行为定义明确(将抛出异常并捕获),表达式*((A*)0)已被评估,因为它不是未评估的一部分操作数。现在,如果通过空指针间接引发UB,那么表达式写为

*((A*)0);

会做到这一点,诱导UB,与typeid场景相比,这似乎是荒谬的。 如果仅仅评估上述表达式,因为每个丢弃值表达式都是 1 ,那么在第二个代码片段UB中进行评估的关键区别在哪里?没有现有的实现分析typeid - 操作数,找到最里面的,相应的取消引用并用一个检查包围它的操作数 - 也会有性能损失。

该问题中的说明随后以:

结束了简短讨论
  

我们同意标准中的方法似乎没问题: p = 0; *p;   本质上不是一个错误。左值到左值的转换会给出   它未定义的行为。

即。委员会对此表示同意。虽然本报告的提议解决方案从未被采纳过,但该报告引入了所谓的&#34; 空左值&#34; ...

  

然而,“不可修改”是一个编译时的概念,而事实上   这会处理运行时值,因此应该生成undefined   行为而不是。此外,还有其他上下文可以使用的上下文   发生,例如左操作数。或。*,也应该是   限制。需要进一步起草。

... 不影响理由。然后,应该注意的是,这个问题甚至先于C ++ 03,这使得它在我们接近C ++ 17时不那么有说服力。


CWG问题#315似乎也涵盖了您的情况:

  

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

  struct A { void f () { } };
  int main ()
  {
    A* ap = 0;
    ap->f ();
  }
     

[...]

     

理由(2003年10月):

     

我们同意应该允许该示例。 p->f()被重写为   根据5.2.5 [expr.ref] (*p).f()*p时不是错误   p为空,除非将左值转换为右值(4.1   [conv.lval]),它不在这里。

根据这个基本原理,通过空指针本身的间接不会在没有进一步的左值到右值转换(=访问存储值),引用绑定,值计算等的情况下调用UB。 (Nota bene:使用空指针调用非静态成员函数应该调用UB,尽管只是被[class.mfct.non-static] / 2勉强禁止。理由在这方面已经过时了。)

即。仅仅*d的评估不足以调用UB。不需要对象的标识,也不是它先前存储的值。另一方面,例如。

*p = 123;
由于存在左操作数的值计算,因此

未定义,[expr.ass] / 1:

  

在所有情况下,分配在值计算之后排序   左右操作数

因为左操作数应该是glvalue,所以glvalue引用的对象的标识必须按[intro.execution] / 12中表达式的评估定义所述来确定,这是不可能的(因此导致UB)。


1 [expr] / 11:

  

在某些情况下,表达式仅出现其副作用。   这种表达式称为丢弃值表达式。的的   表达式被评估并且其值被丢弃。 [...]。左值到右值的转换(4.1)是   当且仅当表达式是glvalue时才应用   volatile限定类型和[...]

答案 1 :(得分:4)

来自C ++草案标准N3337:

  

9.4静态成员

     

2可以使用 qualified-id表达式 static引用类X的{​​{1}}成员;没有必要使用类成员访问语法(5.2.5)来引用X::s成员。可以引用static成员   使用类成员访问语法,在这种情况下,将评估对象表达式。

关于对象表达的部分......

  

5.2.5班级成员访问

     

4如果声明E2具有类型“引用T”,那么E1.E2是左值; E1.E2的类型是T.否则,   适用以下规则之一。

     

- 如果staticE2数据成员且static的类型为E2,则T是左值;表达式指定类的命名成员。 E1.E2的类型为T.

根据标准的最后一段,表达式:

E1.E2

工作是因为无论 d->fun(); std::cout << d->a; 的值是什么,它们都指定了该类的命名成员。

答案 2 :(得分:3)

  

运行正常并产生预期的输出而不是任何运行时错误。

这是一个基本的假设错误。您正在做的是未定义的行为,这意味着您对任何类型“预期输出”的声明有误。

附录:请注意,虽然有一个CWG缺陷(#315)报告因的“协议”而关闭,而上述UB ,它依赖于另一个仍然有效的 CWG缺陷(#232)的正面结束,因此没有一个被添加到标准中。

让我引用James McNellisan answer的评论的一部分到类似的Stack Overflow问题:

  

我认为CWG缺陷315并不像“关闭问题”页面所暗示的那样“关闭”。理由说它应该被允许,因为“除非将左值转换为右值,否则当p为空时,* p不是错误。”然而,这依赖于“空左值”的概念,这是CWG缺陷232的拟议解决方案的一部分,但尚未被采用。

答案 3 :(得分:1)

表达式d->fund->a()都引起对*d([expr.ref] / 2)的求值。

[expr.unary.op] / 1中一元*运算符的完整定义是:

  

一元*运算符执行间接操作:应用该表达式的表达式应为指向对象类型的指针或指向函数类型的指针,并且结果为指向该对象或函数的左值表达式所指向的位置。

对于表达式d,没有“表达式指向的对象或函数”。因此,本段未定义*d的行为。

因此,由于对*d进行评估的行为未在标准的任何地方定义,因此无法通过遗漏来定义代码。

答案 4 :(得分:0)

你在这里看到的是我认为C ++语言规范中的一个构思错误和不幸的设计选择,以及属于同一类编程语言的许多其他语言。

这些语言允许您使用对类实例的引用来引用类的静态成员。当然,实例引用的实际值被忽略,因为访问静态成员不需要实例。

因此,在d->fun();中,编译器仅在编译期间使用d指针 ,以确定您指的是demo类的成员,然后它忽略它。编译器没有发出代码来取消引用指针,因此在运行时它将为NULL并不重要。

所以,你看到的情况完全符合语言的规范,在我看来,规范在这方面受到了影响,因为它允许发生不合逻辑的事情:使用实例引用来引用静态构件。

P.S。大多数语言中的大多数编译器实际上都能够发出针对这类内容的警告。我不知道你的编译器,但你可能想检查一下,因为你没有收到任何关于你所做的事情的警告,这可能意味着你没有启用足够的警告。