为什么在 C++ 中强制使用返回值优化时需要公共析构函数?

38

请考虑以下简单示例,其中函数bar返回一个具有私有析构函数的类A的对象,并且必须进行强制返回值优化(RVO):

class A { ~A() = default; };
A bar() { return {}; }

这段代码被Clang编译器接受,但被GCC编译器以以下错误拒绝:

error: 'constexpr A::~A()' is private within this context
    2 | A bar() { return {}; }
      |                   ^

https://gcc.godbolt.org/z/q6c33absK

这里哪一个编译器是正确的?


VC++ 也接受它。 - Michael Chourdakis
5
根据cppreference,自 C++17 起,在 return 语句处必须能够访问析构函数,这意味着 Clang 是错误的,而 GCC 是正确的。 - Nathan Pierson
4
@NathanPierson听起来像是一个答案。 - alter_igel
@alterigel 更有趣的是展示为什么法定答案被编码化。 - Deduplicator
2个回答

48

这是CWG 2426。在这种情况下可能会调用析构函数,因为即使在返回A对象初始化之后,函数仍然有可能无法成功完成:在return语句期间创建的临时变量和所有处于作用域内的自动局部变量都必须被销毁,如果析构过程抛出异常,则A对象将在堆栈展开的过程中被销毁。编译器应该要求此时可以访问析构函数。

注1:函数最外层作用域的局部变量的析构函数抛出的异常可以被函数try块捕获。

注2:在返回对象被销毁后,处理程序允许执行另一个return语句。标准中有一个例子。


3
在这个特定的背景下(OP的例子),可以轻松地证明析构函数永远不会被调用。其他示例则没有那么好。 - Deduplicator
7
只有在由于另一个异常而进行堆栈展开时才适用。这里不适用。 - Deduplicator
@Deduplicator,标准在许多方面都不是这样“工作”的。例如,取决于启用/禁用异常处理和优化级别,您可能会发现数十种情况,其中许多标准要求对您的特定代码部分可能不相关。对于这个方面,如果有疑问,几乎不可能输出合理的编译器诊断,因此为什么公开可用的析构函数对于特定的代码示例是必需的,这一点需要进一步说明。 - Secundi
@Secundi 我不确定你指的是我哪些评论。关于第一个:在创建返回值后,这个特定函数没有失败的可能性,因为这是它唯一做的事情。因此,析构函数永远不会被调用。为什么它仍然可能被调用是个有趣的问题。关于第二个:那应该很简单。另外,禁用异常意味着非标准,显然也意味着我第二个评论中的触发器无法应用。 - Deduplicator
@Deduplicator 是的,第一个。如果您希望将其包含在标准中,那么如何正式化?“如果编译器可以轻松证明析构函数实际上从未被使用(即汇编输出分析存在疑问...),那么您就不需要它”?听起来相当模糊。 - Secundi
显示剩余3条评论

15
在像问题中这样的简单情况下,可以很容易地证明析构函数永远不会被使用,但是代码确实被使用了。
然而,决定这个问题可能变得任意复杂,这是标准化的祸根。把它留给实现者处理将分裂语言,创建不兼容的子方言,因为他们会花费不同的努力来决定(不同的)边角情况。
但即使这还不是问题的终点,因为解决这个问题意味着解决停机问题,因此不仅是难以处理,而是无法决定的。
因此,CWG 2426所做的回避不仅是出于理智(指定所有细节非常快速),而且是没有选择,在指定任意数量的任意选择的简单情况之后,恣意地划线。

+1 提到停机问题!我认为委员会在形式化方面的核心问题是:如果析构函数的公共可用性可能是必需的,在优化级别和启用/禁用异常处理方面存在疑问,那么类设计本身将取决于这些方面,更不用说可怕的编译器诊断了。 - Secundi
这根本不是事实。在 CWG2426 之前,由 CWG2176 添加的措辞将决定析构函数是否可能被调用,不需要额外的措辞。只有当返回后销毁的任何东西(返回语句中的临时变量 + 自动变量)具有 noexcept(false) 析构函数类型时,编译器必须在编译返回语句后需要调用哪些析构函数时枚举它们。 - Artyer

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