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

29

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

#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
我的程序有什么问题吗?它仍然存在未定义行为。 - user1810087
12
未定义行为并不意味着代码必须崩溃;相反,它意味着任何事情都可以发生,结果是未定义的。也就是说,代码可能看起来正常工作并且符合预期,也可能崩溃,可能看起来运行正常但给出错误的结果,任何事情都有可能发生。 - wolfPack88
4
投票通过重新开放;链接的问题涉及非静态成员,而不是静态成员。 - T.C.
2
这是一个有趣的问题 - 它可以干净地编译且没有警告,并调用了正确的函数。但它的语法是否合法?如果忽略d为null,如果d是有效指针会发生什么?看到d->f()调用一个静态函数确实令人惊讶。 - pm100
3
最大的问题在于可维护性。应该是 demo::f() 和 demo::a,如果稍后有人编辑代码,他们可能会尝试使用该指针。 - Kenny Ostrom
显示剩余7条评论
5个回答

33

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;
在这个语句中,根据[stmt.expr],*d是一个被丢弃的值表达式。因此,*d仅会被求值一次,就像d->a一样。
因此,如果*d;是有效的,或者换句话说,表达式*d的求值是有效的,则你的示例也是有效的。

通过空指针进行间接引用本质上会导致未定义的行为吗?

已经有一个开放的CWG问题#232超过十五年,涉及到了这个确切的问题。提出了一个非常重要的论点。报告开始于

至少在IS的一些地方,规定通过null指针间接引用会产生未定义的行为:1.9 [intro.execution]第4段以“取消引用null指针”作为未定义行为的例子,而8.3.2 [dcl.ref]第4段则使用这种被认为是未定义行为来证明“null引用”的不存在。

请注意,所提到的示例已更改为覆盖对const对象的修改,而[dcl.ref]中的注释 - 虽然仍存在 - 不是规范性的。规范性的条款已被删除以避免承诺。

然而,描述一元"*"运算符的5.3.1 [expr.unary.op]第1段,并没有说如果操作数是null指针则行为未定义,这可能出人意料。此外,至少有一个段落给出了取消引用空指针的明确定义行为:5.2.8 [expr.typeid] 第2段说

如果lvalue表达式是通过将一元*运算符应用于指针获得的,并且指针是null指针值(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);

typeid情况相比,仅仅进行抛弃值表达式的计算,这时如果在第二段代码片段中进行评估的关键区别是什么,导致产生未定义行为?已经存在的实现不能分析typeid操作数,找到最内层对应的解引用并用检查语句包围其操作数 - 这也会带来性能损失。

然后,该问题的一个注释以以下方式结束了简短的讨论:

我们一致认为标准中的方法是正确的:p = 0; *p;本质上不是错误的。将lvalue转换为rvalue将产生未定义行为。

也就是说,委员会对此达成了一致意见。尽管这份报告提出的解决方案——所谓的“空lvalue”——从未得到采纳......

不过,“不可修改”是一个编译时概念,而实际上这涉及运行时值,因此应当产生未定义行为。此外,在其他上下文中也可能出现lvalue,例如 . 或 .* 的左操作数,也应受到限制。需要进行额外的起草。

......这不影响基本原理。然而,需要注意的是,这个问题甚至在C++03之前就存在了,这在我们接近C++17时可能不太令人信服。


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

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

  struct A { void f () { } };
  int main ()
  {
    A* ap = 0;
    ap->f ();
  }
根据这个解释,仅仅通过空指针进行间接引用本身不会触发未定义的行为,除非进行了进一步的左值到右值转换(即访问存储的值),引用绑定、值计算或类似操作。(须知:使用空指针调用一个非静态成员函数应该会触发未定义的行为,尽管这在 [class.mfct.non-static]/2 中模糊地被禁止。在这方面,这个解释已经过时。)换句话说,仅仅评估 *d 不足以触发未定义的行为。不需要知道对象的身份,也不需要先前存储的值。另一方面,例如:
*p = 123;

由于左操作数和右操作数的计算结果顺序已确定,根据[expr.ass]/1:

在所有情况下,赋值操作在右操作数和左操作数的值的计算之后进行。

由于左操作数应为glvalue(广义左值),根据[intro.execution]/12中表达式求值的定义,必须明确确定glvalue所引用的对象的标识符,但这是不可能的(从而导致未定义行为)。


1 根据[expr]/11:

在某些上下文中,表达式仅出现以产生副作用。这种表达式称为“放弃值表达式”。计算表达式并丢弃其值。 如果表达式是volatile限定类型的glvalue,则应用lvalue-to-rvalue转换(4.1),条件是[...]


4
似乎这项决议从未成为任何标准。可能是因为它对于参考文献意义的影响。 - Deduplicator
2
"当没有进一步的左值到右值转换(=访问存储值)或引用绑定时,取消引用空指针不会调用未定义行为"这是一些人想要的,而不是实际情况。你对引用进行了扩展,感到满意吗? - Deduplicator
1
这个程序的行为是明确定义的,但不是由于任何一般规则,而是由于你引用的typeid的特定和狭窄的例外。 - Deduplicator
3
Columbo,Clang和GCC都有一整套的调试选项。请访问 https://gcc.gnu.org/onlinedocs/gcc/Debugging-Options.html 并搜索“-fsanitize”以获取相关信息。 - T.C.
2
@Columbo 一个将定义良好的代码转化为错误的消毒剂在我看来会非常令人恼火。无论如何,如果只允许“疯狂”的代码,则我认为标准没有理由不限制它。 - T.C.
显示剩余30条评论

4

根据C++草案标准N3337中的内容:

9.4 静态成员

2 类Xstatic成员s可以使用限定符表达式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的值。


1
@KennyOstrom:抱歉,我没有看到任何要求在该引用中忽略由表达式E1调用的UB。 - Deduplicator
1
如果是这样的话,他们会将其指定为“未评估上下文”,但他们明确没有这样做。 - Deduplicator
1
@RSahu:将其设置为未评估状态会引发一系列棘手的问题,因为将函数设置为静态或非静态将极大地且意外地改变代码,特别是在模板中。而允许“空的左值”也是他们没有做的事情,因为这也会产生深远的不良后果。 - Deduplicator
4
@RSahu 如果LHS是静态的,仍需要对其进行评估,否则 g().f() 可能不会评估 g() - T.C.
2
尽管表达式指定了类的命名成员,但这并不会“绕过”对E1的评估。更明显的例子是g()->a,其中g例如除以零,然后返回一个空指针。 - M.M
显示剩余6条评论

4

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

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

补充:请注意,虽然有一个CWG缺陷(#315)报告关闭了与使上述UB相同的意见,但它依赖于另一个仍然活动的CWG缺陷(#232)的积极关闭,因此没有将其添加到标准中。

让我引用James McNellis一个类似的Stack Overflow问题的答案中发表的评论的一部分:

我认为CWG缺陷315并不像它在“已关闭问题”页面上的存在暗示的那样“已关闭”。其原理是说应该允许它,因为“* p在将lvalue转换为rvalue之前,当p为null时不是错误。”但是,这依赖于“空lvalue”的概念,它是对CWG缺陷232的建议解决方案的一部分,但尚未被采用。

3
如果你能证明那些决议曾经被纳入标准中,你就有一定的观点。 - Deduplicator
@Columbo:我添加了一段来自James McNellis的补充说明,阐明了为什么你的答案并没有反驳它是未定义行为的事实。 - Johann Gerell
现在这是唯一正确的答案。真遗憾我不能再次点赞它了。 - Deduplicator
有没有任何理由,使得在这种情况下优质的实现应该关心实例?我想警告一下,代码可能会被编译器设计人员破坏,他们以找到“聪明”的方法来避免让他们的编译器执行任何标准未规定的操作而自豪。另一方面,标准只定义了一个“符合”实现,而不是一个“其有用性不受晦涩影响的实现”;前者允许某些特定行为并不意味着后者也能表现出同样的行为。 - supercat

1

你所看到的是我认为在C++语言规范和许多其他属于同一类编程语言家族的语言中,一个不合理且不幸的设计选择。

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

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

因此,您看到的情况完全符合语言规范,并且在我看来,规范在这方面存在问题,因为它允许发生不合逻辑的事情:使用实例引用来引用静态成员。

顺便说一句,大多数语言中的大多数编译器实际上都能够为那种东西发出警告。我不知道你的编译器,但你可能想检查一下,因为你没有收到关于你所做的警告可能意味着你没有足够的警告启用。


您提出的更改可能会破坏现有代码。template <class T> f(T &t) { t.g(); } struct StillWorking { void g() {} }; struct NowBroken { static void g(); } }; f(StillWorking()); f(NowBroken()); - Christian Hackl
4
“使用实例引用来引用静态成员应该是不可能的。” 这艘船早已在很久以前就开走了。 - T.C.
PS:我本来想写一个带有const&的例子(我的代码无论如何都无法编译),但是重点仍然存在。 - Christian Hackl
静态成员仍然是成员,因此无论它是静态成员还是非静态成员,this->foo 都是可以的。这是一种有意的设计选择,而不是一个错误。 - Jonathan Wakely
我给你的回答清晰简洁,对OP的问题有所帮助。我给你点赞。 - ManuelH
显示剩余4条评论

1
表达式d->fund->a()都会导致*d([expr.ref]/2)的求值。
从[expr.unary.op] / 1完整定义一元运算符*是:
一元运算符*执行间接引用:应用它的表达式必须是指向对象类型或指向函数类型的指针,并且结果是引用表达式指向的对象或函数的lvalue。
对于表达式d,没有“表达式指向的对象或函数”。因此,该段不定义*d的行为。
因此,代码未定义,因为在标准中没有定义评估*d的行为。

1
@HolyBlackCat 没错,但我的答案取决于“表达式指向的对象或函数”的文本。 - M.M

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接