【问题标题】:c++ access static members using null pointerc++ 使用空指针访问静态成员
【发布时间】:2015-04-13 12:05:34
【问题描述】:

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

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

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

【问题讨论】:

  • Is there any harm in my program?还是UB。
  • 未定义的行为并不意味着代码需要崩溃;相反,它意味着任何事情都被允许发生,结果是未定义的。也就是说,代码可能看起来工作正常,并且正如预期的那样,它可能会崩溃,它可能看起来运行正常,但会给你错误的结果,什么都没有。
  • 投票重新开放;链接的问题针对的是非静态成员,而不是静态成员。
  • 这是一个有趣的问题——它编译干净,没有警告,它调用了正确的函数。它是有效的语法吗?忘记 d 为空,如果 d 是有效指针怎么办。令人惊讶的是 d->f() 其中 f 是一个静态函数
  • 最大的问题是可维护性。它应该是 demo::f() 和 demo::a,如果有人稍后编辑代码,他们可能实际上会尝试使用该指针。

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


【解决方案1】:

TL;DR:您的示例定义明确。仅仅解除对空指针的引用不会调用 UB。

关于这个话题有很多争论,基本上归结为通过空指针间接是否本身就是UB。
在您的示例中发生的唯一值得怀疑的事情是对象表达式的评估。特别是,d-&gt;a 等价于 (*d).a 根据 [expr.ref]/2:

表达式E1-&gt;E2 被转换为等价形式 (*(E1)).E2; 5.2.5 的其余部分将只解决第一个 选项(点)。

*d 刚刚被评估:

计算点或箭头之前的后缀表达式;65 该评估的结果与 id-expression 一起确定 整个后缀表达式的结果。

65) 如果评估类成员访问表达式,即使结果是不必要的,也会发生子表达式评估 确定整个后缀表达式的值,例如如果 id-expression 表示静态成员。

让我们提取代码的关键部分。考虑表达式语句

*d;

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

通过空指针进行间接寻址是否会固有地导致未定义的行为?

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

开头

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,那么在第二个 sn-p UB 中进行评估的关键区别在哪里? 没有分析typeid-操作数的现有实现,找到最里面的相应解引用并用检查包围其操作数 - 也会有性能损失。

该问题中的注释然后结束了简短的讨论:

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

即委员会同意这一点。尽管本报告提出的决议,其中引入了所谓的“空左值”,但从未被采纳……

然而,“不可修改”是一个编译时概念,而事实上 这处理运行时值,因此应该产生 undefined 行为。此外,在其他情况下,左值可以 发生,例如 的左操作数。或 .*,也应该是 受限制的。需要额外起草。

不影响理由。话又说回来,应该注意的是,这个问题甚至早于 C++03,这使得在我们接近 C++17 时它不太令人信服。


CWG-issue #315 似乎也涵盖了您的情况:

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

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

[…]

基本原理(2003 年 10 月):

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

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

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

*p = 123;

是未定义的,因为有一个左操作数的值计算,[expr.ass]/1:

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

因为左操作数应该是一个glvalue,这个glvalue引用的对象的标识必须按照[intro.execution]/12中表达式求值的定义中提到的那样确定,这是不可能的(从而导致UB)。


1 [expr]/11:

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

【讨论】:

  • 看起来好像该分辨率从未将其纳入任何标准。可能是因为它对参考意味着什么......
  • 如果您知道该决议从未成为法律,那么为什么要引用它呢?最好找一些可以做到的,特别是因为那个人已经 13 岁以上(临时有两个小标准和一个大标准)。
  • “在没有进一步的左值到右值转换(=访问存储值)或引用绑定的情况下,取消引用空指针不会调用 UB”这是一些人想要的,而不是实际的。对我延长报价感到满意吗?
  • @Columbo Clang 和 GCC 有一整套。 gcc.gnu.org/onlinedocs/gcc/Debugging-Options.html,搜索-fsanitize
  • @Columbo 将定义明确的代码变成错误的消毒剂将非常烦人 IMO。无论如何,我认为标准没有理由不限制它,如果允许它只允许“疯狂”的代码。
【解决方案2】:

来自 C++ 草案标准 N3337:

9.4 静态成员

2 staticX 的成员可以使用 qualified-id 表达式 X::s 来引用;不必使用类成员访问语法 (5.2.5) 来引用 static 成员。可以推荐static 成员 到使用类成员访问语法,在这种情况下对象表达式被求值。

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

5.2.5 班级成员访问权限

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

——如果E2是一个static数据成员并且E2的类型是T,那么E1.E2是一个左值;表达式指定类的命名成员。 E1.E2 的类型是 T。

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

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

之所以有效,是因为它们都指定了类的命名成员,而不管d 的值如何。

【讨论】:

  • "如果 E2 是静态数据成员 ... 表达式指定命名成员。"它就在那里说,这是有道理的,因为指针完全不相关。
  • @KennyOstrom:对不起,我没有看到任何忽略该引用中 E1 表达式调用的 UB 的授权。
  • @RSahu:如果是这样的话,他们会将其指定为“未评估的上下文”,但他们明确没有这样做。
  • @RSahu . 的 LHS 需要评估,即使它是静态的,或者 g().f() 可能无法评估 g()
  • 即使表达式指定了类的命名成员,也不会“绕过”E1 的评估。一个更明显的例子,g()-&gt;a,例如 g 除以零,然后返回一个空指针
【解决方案3】:

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

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

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

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

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

【讨论】:

  • @Columbo:现在,如果你能证明这些决议曾经将其纳入标准,你就会有道理。
  • @Columbo:我添加了 James McNellis 的简介附录,阐明了为什么您的回答没有质疑它是 UB。
  • 现在这是唯一正确的答案。很遗憾我不能再次投票。
  • 在这种情况下,quality 实现是否应该关心实例?我想警告代码可能会被编译器设计者破坏可能是公平的,他们以找到“聪明”的方法来避免让他们的编译器做任何标准没有规定的事情而自豪。另一方面,该标准只定义了一个“符合”的实现,而不是一个“其实用性不会被钝性破坏的实现”;前者允许某种特定行为这一事实并不意味着后者也可以表现出同样的行为。
【解决方案4】:

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

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

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

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

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

【讨论】:

  • @HolyBlackCat 没错,但我的答案取决于文本“表达式指向的对象或函数”
【解决方案5】:

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

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

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

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

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

【讨论】:

  • 您提议的更改可能会破坏现有代码。 template &lt;class T&gt; f(T &amp;t) { t.g(); } struct StillWorking { void g() {} }; struct NowBroken { static void g(); } }; f(StillWorking()); f(NowBroken());
  • "应该不可能使用实例引用来引用静态成员。"那艘船很久很久以前就航行了。
  • P.S.:我打算用const&amp; 写一个例子(我的无论如何都不会编译),但重点是。
  • @T.C.是的,它已经航行了,但我是否有权认为它不应该是这样的?
  • @ChristianHackl 如果我写的是“这应该是不可能的”而不是“这应该是不可能的”,你会满意吗?
猜你喜欢
  • 2020-10-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-01-02
相关资源
最近更新 更多