【问题标题】:Function parameters are not destroyed at that function's exit函数参数不会在该函数退出时销毁
【发布时间】:2014-11-28 20:59:42
【问题描述】:

我很确定,函数参数的析构函数应该在相应函数的出口处调用。考虑 C++11 标准的 5.2.2p4:

[...] 参数的生命周期在定义它的函数返回时结束。 [...]

不过,让我们试试这段代码:

#include <iostream>
using namespace std;

struct Logger {
    Logger(int) { cout << "Construct " << this << '\n'; }
    Logger(const Logger&) { cout << "Copy construct " << this << '\n'; }
    ~Logger() { cout << "Destruct " << this << '\n'; }
};

int f(Logger)
{
    cout << "Inside f\n";
    return 0;
}

int main()
{
    f(f(f(10)));
}

用gcc或clang编译后,输出如下:

Construct 0x7fffa42d97ff
Inside f
Construct 0x7fffa42d97fe
Inside f
Construct 0x7fffa42d97fd
Inside f
Destruct 0x7fffa42d97fd
Destruct 0x7fffa42d97fe
Destruct 0x7fffa42d97ff

正如我们所见,所有三个参数仅在最后一个函数调用完成后才被销毁。这是正确的行为吗?

【问题讨论】:

  • 另一个问题的总结:您阅读标准是完全正确的,但标准并没有正确反映意图,意图也是允许您的编译器的行为。跨度>
  • “我们可以看到,所有三个参数都是在最后一次函数调用完成后才被销毁的。” 我们看不到这样的事情。我确定你误解了这个输出。
  • @LokiAstari That other question 一开始就不应该关闭,但即使关闭了,那又有什么区别呢?无论如何,它确实提出了同样的问题,并且那里的答案确实为这个问题提供了答案。
  • @hvd:不,允许复制省略以消除这种副作用。是的,在main 中构建了绝对临时的。文字10(以及每个调用的返回值)用于构造一个值以复制到f 的参数中。如果只有 OP 引用了 整个 段落。
  • @dyp 是编译器不符合C++11的领域,but the standard is being changed to allow the compilers' behaviour

标签: c++


【解决方案1】:

参见 C++11 标准,§12.2/3,说

临时对象在评估的最后一步被销毁 full-expression (1.9) (词法上)包含它们所在的点 已创建。

【讨论】:

  • 我不认为函数的参数算作临时对象。至少它们没有在 12.2p1 中列为上下文。
  • 恕我直言 12.2p2 清楚地表明函数参数和函数返回值可能是临时的。
  • 我不同意,请阅读此示例下方的文本:“在使用 X 的复制构造函数将 X(2) 传递给 f() 之前,实现可能会使用临时构造 X(2);或者, X(2) 可能在用于保存论点的空间中构造。”这个例子是关于如何使用或避免临时创建来传递函数参数(复制省略)。
  • 该标准的要求很明确,并且已经在问题中准确引用。函数参数不是临时对象。
  • 我认为这不能完全回答这个问题。如果您删除复制/移动省略,则函数参数仍然被后期销毁,即使它们不是临时的:coliru.stacked-crooked.com/a/21a091103ce7a383
【解决方案2】:

从目前已经可以在 cmets 中找到的内容进行详细说明:

给定int f(Logger);,当你写的时候:

f(10);

this(概念上)构造一个临时的Logger对象,从该临时对象构造函数参数,调用函数,销毁函数参数,最后销毁临时对象。

当你写作时:

f(f(10));

this(从概念上讲)构造一个临时 Logger 对象,从该临时对象构造函数参数,调用函数,销毁函数参数,使用第一个函数调用的结果构造一个新的临时 Logger 对象,构造来自那个临时对象的函数参数,调用函数,销毁函数参数,最后销毁两个临时对象。

我会避免为f(f(f(10))); 的情况写出来。

现在,这两个临时对象可以省略:

当满足某些条件时,允许实现省略类对象的复制/移动构造,即使对象的复制/移动构造函数和/或析构函数有副作用。在这种情况下,实现将省略的复制/移动操作的源和目标简单地视为引用同一对象的两种不同方式,并且该对象的销毁发生在两个对象本应被删除的较晚时间。没有优化就被破坏了。这种复制/移动的省略 称为复制省略的操作在以下情况下是允许的(可以组合起来消除多个副本):

  • ...

  • 当尚未绑定到引用 (12.2) 的临时类对象将被复制/移动到具有相同 cv-unqualified 类型的类对象时,可以通过构造临时对象直接进入省略复制/移动的目标

  • ...

由于函数参数和临时对象具有相同的类型,因此允许编译器将它们视为相同的对象。临时对象将在最后阶段被销毁,因此参数的生命周期不会起作用。

但是,当复制省略执行时,例如因为您将编译器配置为不执行,或者因为首先没有复制要删除(见下文),那么函数当你说它们应该被销毁时,参数确实必须被销毁,并且你必须在所有符合 C++11 实现的第二个函数调用开始之前看到“Destruct (...)”。

使用大括号可以在没有临时参数的情况下构造参数:您可以将调用重新工作为

f({f({f({10})})});

这里,每个参数都是列表初始化的,在这种情况下不涉及临时对象,并且没有要删除的副本。这必须在函数 f 返回时立即销毁函数参数,在再次调用 f 之前,在所有符合 C++11 的实现中,不管任何 -felide-constructors 命令行选项,而编译器不这样做是它们不符合 C++11 的一个领域。

这并不是相当那么简单,不过:CWG issue 1880 读作:

WG 决定不指定参数对象是在调用后立即销毁还是在调用所属的完整表达式的末尾销毁。

这将允许编译器现在做的事情:参数可以在完整表达式结束后销毁,在最后一个 f 返回之后。 C++11 的确切文字文本不是当前编译器实现的。

【讨论】:

  • 是的,这是一个很好的观点,当复制省略起作用时,应该延长参数的生命周期以满足标准要求。
  • 你能说几句低级发生的事情吗?不用考虑内联,只是一个普通的函数调用。我不知道汇编程序,但据我了解,当我们按值传递参数时,它应该被构造为 调用 函数堆栈框架的一部分,因此,必须将其销毁为这个函数的返回机制的一部分。然而,这并没有发生,那么它是如何工作的呢?我们是传递一个指针而不是参数本身,还是将参数保留在我们的堆栈中,直到完整表达式结束?
  • @BevelLemelisk 该标准规定参数构造和销毁发生在调用函数中。你是对的,许多 ABI 让参数破坏发生在被调用者中,ABI 解决这个问题的方法是确定一个类型是否有一个微不足道的析构函数(还有其他考虑因素),如果没有,就像你怀疑的那样,不要t 传递类型,传递对类型的引用,传递给调用者构造和破坏的内容。请参阅Itanium C++ ABI - Value Parameters 了解特定 ABI 的确切要求。
  • 因此,对于任何非TriviallyCopyable 类型,按值传递它与通过引用传递它几乎相同(就生成的汇编代码而言)(当然,如果临时是为此通过引用创建)?在此之前,我很确定,按值传递在这里消除了一层间接性。感谢您提供有趣的信息。
  • @BevelLemelisk 差不多,但要求与非平凡可复制类型的要求并不完全相同。具有普通复制构造函数和普通析构函数但具有非普通赋值运算符的类型不是普通可复制的,但(在该 ABI 中)与普通可复制类型的传递方式相同。
【解决方案3】:

你期望看到的是:

Construct 0x7fffa42d97ff         //  Creation of temporary object
Copy construct 0xAAAAAAAAAA      //  copy constuction of parameter
Inside f
Destruct 0xAAAAAAAAAA            // destruction of parameter.
Construct 0x7fffa42d97fe
Copy construct 0xBBBBBBBBB
Inside f
Destruct 0xBBBBBBBBB
Construct 0x7fffa42d97fd
Copy construct 0xCCCCCCCCC
Inside f
Destruct 0xCCCCCCCCC
Destruct 0x7fffa42d97fd
Destruct 0x7fffa42d97fe
Destruct 0x7fffa42d97ff          // destruction of temporary

但允许编译器省略(删除)参数(及其析构函数)的复制构造并内联函数。如果这样做,剩下的唯一构造的对象就是传递给函数的临时对象。

因此,如果您使用我的结果集删除复制构造(由激进的编译器优化引起),您将得到您在答案中看到的输出。

如果你想看到上面的输出。然后阻止编译器内联函数:见https://stackoverflow.com/a/1474050/14065

注意:内联只是省略副本的原因之一。编译器可以使用其他几个。我使用内联示例,因为它最容易可视化删除复制到函数中的参数。

【讨论】:

  • 我认为coliru.stacked-crooked.com/a/21a091103ce7a383 表明(还有?)其他事情正在发生。如果存在复制省略,我希望延长寿命,但我不希望函数参数的后期销毁。我不知道您为什么指的是内联,因为 AFAIK 改变可观察行为也不例外(与复制省略不同)。
  • @dyp:函数参数不会被后期销毁(因为它们将通过复制构造创建)。它们(参数)已被删除(省略)。唯一要被摧毁的是临时工。在语句结束之前不会被销毁。
  • 在现场演示中,我链接的复制省略应该被停用 (-fno-elide-constructors)。因此,表达式f(10) 创建了一个从int prvalue 直接初始化的临时Logger,然后从该临时复制初始化函数参数。临时存在直到call-fullexpression结束,但我认为函数参数应该在返回之前/函数返回时被销毁(这与Logger无关,因为它只在ints上运行) .
  • 当对象的生命周期结束有可观察的影响时,as-if 规则不允许任何对象的生命周期延长。
  • @hvd 但是复制省略允许它。这就是 OP 问题的全部意义(答案):由于临时对象与参数合并(它们是 same 对象),因此该对象是在两个创建中的较早者创建的次,并在两次破坏时间中的较晚者被破坏。作为参考,请参阅 12.8/31 “该对象的销毁发生在两个对象在没有优化的情况下会被销毁的较晚时间。”
【解决方案4】:

这是正确的行为。它遵循破坏资源的先进后出方法。如果您按顺序调用该函数,您会得到不同的结果。

f(10)
f(10)
f(10)

会这样破坏:

Construct 0x7fffa42d97ff
Inside f
Destruct 0x7fffa42d97ff
Construct 0x7fffa42d97fe
Inside f
Destruct 0x7fffa42d97fe
Construct 0x7fffa42d97fd
Inside f
Destruct 0x7fffa42d97fd

【讨论】:

  • 我同意,当对象在同一时间点被销毁时,销毁应该与构造相反。但是,当对象在 不同 时间被销毁时,此规则不适用(如您的示例中所示)。而且我很确定,函数参数应该在它们的函数退出时被销毁(这 3 个调用中的每一个都是不同的时间)。
猜你喜欢
  • 1970-01-01
  • 2018-07-05
  • 1970-01-01
  • 2023-01-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-08-27
  • 1970-01-01
相关资源
最近更新 更多