27

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

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

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

4

5 回答 5

32

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()*pp

根据这个原理,如果没有进一步的左值到右值转换(=访问存储的值)、引用绑定、值计算等,通过空指针本身的间接调用不会调用 UB。(注意事项:使用空指针调用非静态成员函数应该调用 UB,尽管 [class.mfct.non-static]/2 只是模糊地不允许这样做。在这方面,基本原理已经过时了。)

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

*p = 123;

未定义,因为存在对左操作数 [expr.ass]/1 的值计算:

在所有情况下,赋值都是在左右操作数的值计算之后排序的

因为左操作数应该是一个glvalue,所以该glvalue引用的对象的身份必须由[intro.execution] / 12中表达式的评估定义中提到的那样确定,这是不可能的(因此导致到 UB)。


1 [expr] / 11:

在某些情况下,表达式只出现在它的副作用上。这样的表达式称为弃值表达式计算表达式并丢弃其值。[…]。当且仅当表达式是 volatile 限定类型的左值并且 […]

于 2015-02-12T16:54:57.077 回答
4

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

这是一个基本的假设错误。您所做的是未定义的行为,这意味着您对任何类型的“预期输出”的主张都是错误的。

附录:请注意,虽然有一个 CWG 缺陷 ( #315 ) 报告以“同意”进行上述 UB 的方式关闭,但它依赖于另一个仍处于活动状态的CWG 缺陷 ( #232 ) 的积极关闭,因此没有一个被添加到标准中。

让我引用James McNellis对类似 Stack Overflow 问题的回答的部分评论:

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

于 2015-02-12T17:04:51.813 回答
4

来自 C++ 标准草案 N3337:

9.4 静态成员

2可以使用qualified-id 表达式来引用static类的成员;不必使用类成员访问语法 (5.2.5) 来引用成员。可以使用类成员访问语法来引用成员,在这种情况下评估对象表达式。X X::sstaticstatic

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

5.2.5 类成员访问

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

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

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

  d->fun();
  std::cout << d->a;

之所以起作用,是因为它们都指定了类的命名成员,而不管d.

于 2015-02-12T16:54:52.350 回答
1

表达式d->fund->a()都导致*d([expr.ref]/2) 的评估。

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

一元运算*符执行间接:应用它的表达式应该是一个指向对象类型的指针,或一个指向函数类型的指针,结果是一个左值,指向表达式指向的对象或函数。

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

因此,代码因遗漏而未定义,因为评估的行为*d在标准中的任何地方都没有定义。

于 2019-11-17T22:34:42.933 回答
0

您在这里看到的是我认为在 C++ 语言和属于同一通用编程语言家族的许多其他语言的规范中的一个考虑不周和不幸的设计选择。

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

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

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

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

于 2015-02-12T16:49:25.387 回答