【问题标题】:C++: Destructor being called outside object scope?C ++:在对象范围之外调用析构函数?
【发布时间】:2014-10-08 05:09:42
【问题描述】:

更新 1: 按建议添加了打印“this”。

更新 2: 拆分为多个文件以尝试阻止 gcc 优化。

更新 3: 记录复制构造函数并输入添加函数。

更新 4:添加了 Clang 的输出和 main 中的第二个 cout。

我希望参数析构函数作为函数中的最后一个语句被调用。从今以后,我希望从下面的代码中得到以下输出。

default constructor: 008DFCF8
other constructor: 008DFCEC
copy constructor: 008DFBC0
in member add
destroying: 008DFBC0
copy constructor: 008DFBB8
copy constructor: 008DFBB4
in function add
destroying: 008DFBB4
destroying: 008DFBB8
3 == 3
end of main
destroying: 008DFCEC
destroying: 008DFCF8

使用 MSVC (Visual Studio) 时,输出与预期一致。但是 GCC (4.8.2-19ubuntu1) 输出以下内容,表明函数参数的析构函数在 main() 中的第一个 cout 语句之后但在最后一个语句之前被调用。

default constructor: 0x7fff2fcea510
other constructor: 0x7fff2fcea520
copy constructor: 0x7fff2fcea550
in member add
copy constructor: 0x7fff2fcea540
copy constructor: 0x7fff2fcea530
in function add
3 == 3
destroying: 0x7fff2fcea530
destroying: 0x7fff2fcea540
destroying: 0x7fff2fcea550
end of main
destroying: 0x7fff2fcea520
destroying: 0x7fff2fcea510

对于那些对 clang++ (3.4-1ubuntu3) 输出感到好奇的人。

default constructor: 0x7fff52cf9878
other constructor: 0x7fff52cf9870
copy constructor: 0x7fff52cf9860
copy constructor: 0x7fff52cf9858
in function add
3 == copy constructor: 0x7fff52cf9850
in member add
3
destroying: 0x7fff52cf9850
destroying: 0x7fff52cf9858
destroying: 0x7fff52cf9860
end of main
destroying: 0x7fff52cf9870
destroying: 0x7fff52cf9878

问题:

  1. 我最初的怀疑是 GCC 是内联函数?如果这是真的,是否有办法禁用此优化?
  2. C++ 规范中的哪个部分允许在 main 中的 cout 之后调用析构函数?尤其令人感兴趣的是内联规则(如果相关)以及何时安排析构函数。

// Test.h
#ifndef __TEST_H__

#include <iostream>

using namespace std;

class Test
{
public:
    int val;

    Test(Test const &a) : val(a.val)
    {
        cout << "copy constructor: " << this << endl;
    }

    Test() : val(1)
    {
        cout << "default constructor: " << this << endl;
    }

    Test(int val) : val(val)
    {
        cout << "other constructor: " << this << endl;
    }

    ~Test()
    {
        cout << "destroying: " << this << endl;
    }

    int add(Test b);
};

#endif

// Add.cpp
#include "Test.h"

int Test::add(Test b)
{
    cout << "in member add" << endl;
    return val + b.val;
}

int add(Test a, Test b)
{
    cout << "in function add" << endl;
    return a.val + b.val;
}

// Main.cpp
#include "Test.h"

int add(Test a, Test b);

int main()
{
    Test one, two(2);

    cout << add(one, two) << " == " << one.add(two) << endl;

    cout << "end of main" << endl;

    return 0;
}

为 GCC 编译使用:

g++ -c Add.cpp -o Add.o ; g++ -c Main.cpp -o Main.o ; g++ Add.o Main.o -o test

【问题讨论】:

  • GCC 通过在编译期间用3 替换它们来优化对add 的调用。尝试在单独的单元中编译 add 并将两者链接起来。
  • 现在,您知道为什么在输出中只看到两个构造函数和 5 个析构函数吗?你能分辨出哪些对象对应于输出中的每一行吗?
  • 我将代码拆分为几个文件,但使用 GCC 仍然得到相同的结果。我也不明白为什么有 2 个构造函数但有 5 个析构函数。至少 GCC 和 MVSC 在这点上是一致的。
  • 也许你可以澄清“表明析构函数在对象超出范围后被调用”。是什么让你这么想?您认为哪些对象超出了范围,在哪里?
  • 析构函数的日志比构造函数多,因为编译器生成的复制构造函数没有日志记录。

标签: c++ destructor


【解决方案1】:

似乎 C++ 标准对于何时必须调用函数参数析构函数可能有点模棱两可。 C++03 和 C++11 都在 5.2.2/4“函数调用”(强调添加)中说:

参数的生命周期在它所在的函数时结束 定义的回报。各个参数的初始化和销毁 发生在调用函数的上下文中。

因此,参数的析构函数在概念上不会出现在函数的右大括号处。这是我不知道的。

该标准给出了一个注释,解释这意味着如果参数的析构函数抛出,则只考虑调用函数或“更高”的异常处理程序(特别是,即使被调用函数具有“function-try-块',不考虑)。

虽然我认为其意图是针对 MSVC 行为,但我可以看到有人可能会如何解释允许 GCC 行为的读数。

再一次,也许这是 GCC 中的一个错误?

【讨论】:

  • 我在想例如string::c_str(),其中string 是临时的。 GCC 似乎采取了更安全的方式,而不是过早地破坏string
  • @firda:这是一个临时构造(没有为调用c_str() 成员函数构造参数)。 C++ 保证临时对象将一直存在到它们所在的完整表达式的末尾。GCC 似乎正在为其构造的参数执行此操作以调用函数,这是不同的,但上述措辞可能允许这样做。
  • 为了让自己清楚,想象一下:cout &lt;&lt; string("hello").c_str()。但我想我们彼此理解。我很惊讶 VC 这么早调用析构函数,这么晚才写“3 == 3”。
  • @MichaelBurr 那么 c++ 规范中的哪个部分允许编译器在需要时安排“副作用”(构造/破坏)?
  • 我确实认为@firda 是在做某事,因为似乎 GCC 正在处理调用函数参数的生命周期,类似于需要处理临时变量的生命周期。就个人而言,这不是我解释 5.2.2/4 的方式,但我可以看到一个可能。我想知道clang如何处理这个(我没有安装它)?并不是说“编译器投票”必须确定什么是正确的,但是......
【解决方案2】:

在调用“add(a,b)”和调用成员 add(b) 时创建临时对象。 我认为您在 gcc 的情况下看到的是 add() 函数(参数)中的局部变量在这些函数返回时被破坏。 最后两行“完成”用于变量“一”和“二”。

VC 是不同的——但这并没有错,它只是表明两个编译器正在以不同的方式优化代码。

不要只打印“done”,还可以尝试打印值“this”。在构造函数中也打印“this”,然后可以看到构造函数和析构函数调用是如何配对的。

糟糕 - 我在 VC 和 GCC 之间混淆了一点。 VC 首先打印“done” 3 次 - 大概是因为 add() 参数被销毁,而 GCC 最后打印它们,可能是因为它内联了 add 函数。

【讨论】:

  • 有没有办法阻止 GCC 内联调用?我试过 -O0 但这没有用。那么当一个函数被内联时,析构函数会在调用函数通常调用析构函数的地方而不是在内联函数的末尾被调用?
  • @Justin:为什么你还需要特定的析构函数调用模式?
  • @SiyuanRen 我不需要特定的模式,但是老师正在使用 MSVC 编译器,能够重新创建老师期望的相同行为会很有帮助(同时仍在使用海合会)。
【解决方案3】:

考虑这一行:

cout << add(one, two) << " == " << one.add(two) << endl;

写成:

cout << add(one, two);
cout << " == " << one.add(two) << endl;

这会改变 GCC 的打印输出吗?

或者那样:

auto i = add(one, two);
cout << i << " == ";
auto j = one.add(two)
cout << j << endl;

我认为这是关于副作用(不是关于内联)。 VC 似乎更早地安排了副作用(临时对象的破坏),而 GCC 将其安排在语句的末尾 - ;


Added quote:

临时对象生命周期

在各种情况下创建临时对象:绑定一个 引用纯右值,从函数返回纯右值,强制转换为 prvalue,抛出异常,进入异常处理程序,并在 一些初始化。 在任何情况下,所有临时对象都被销毁 评估完整表达式的最后一步 包含创建它们的点,如果有多个 临时对象被创建,它们以相反的顺序被销毁 创作的顺序。即使评估以 抛出异常。

在我看来,这代表 GCC 和反对 VC(尤其是在销毁后打印“3 == 3”,这对我来说很奇怪)。

【讨论】:

  • 你知道 c++ 规范中的哪个部分允许副作用调度的变化吗?
  • 首先,我不完全确定它的命名是side-effects,但有一节讨论序列点(旧式)、序列顺序(前排序,后排序 - 新样式)和未排序(副作用,尤其是后增量):Order of evaluation
猜你喜欢
  • 1970-01-01
  • 2012-09-26
  • 2018-09-15
  • 2023-01-18
  • 2012-06-14
  • 2016-09-26
相关资源
最近更新 更多