3

我正在尝试确定 C++ 函数返回具有构造函数和析构函数的对象是否安全。我对标准的理解是它应该是可能的,但是我用简单的例子进行的测试表明它可能是有问题的。例如下面的程序:

#include <iostream>
using namespace std;

struct My
{  My() { cout << "My constructor " << endl; }
  ~My() { cout << "My destructor " << endl; }
};
My function() { My my; cout << "My function" << endl; return my; }

int main()
{ My my = function();
  return 0;
}

给出输出:

My constructor
My function
My destructor
My destructor

在 MSVC++ 上编译时,但使用 gcc 编译时会给出以下输出:

My constructor
My function
My destructor

这是“未定义行为”的情况,还是其中一个编译器没有以标准方式运行?如果是后者,哪个?gcc 输出更接近我的预期。

迄今为止,我一直在设计我的类时假设每个构造函数调用最多有一个析构函数调用,但这个例子似乎表明这个假设并不总是成立,并且可能依赖于编译器。标准中有什么规定应该在这里发生什么,还是最好避免让函数返回非平凡的对象?抱歉,如果这个问题是重复的。

4

4 回答 4

8

在这两种情况下,编译器都会为您生成一个没有输出的复制构造函数,因此您不知道它是否被调用:请参阅此问题。

在第一种情况下,使用编译器生成的复制构造函数,它与第二个析构函数调用相匹配。该行return my;调用复制构造函数,为其提供my用于构造返回值的变量。这不会产生任何输出。

my然后被销毁。一旦函数调用完成,返回值在行尾被销毁{ function();

在第二种情况下,返回的副本被完全省略(编译器被允许这样做作为优化)。你只有一个My实例。(是的,即使它改变了程序的可观察行为,也允许这样做!)

这些都可以。虽然作为一般规则,如果您定义自己的构造函数和析构函数,您还应该定义自己的复制构造函数(和赋值运算符,如果您有 c++11,则可能移动构造函数和移动赋值)。

尝试添加你自己的复制构造函数,看看你会得到什么。就像是

My (const My& otherMy) { cout << "My copy constructor\n"; }
于 2013-07-16T13:28:01.087 回答
3

问题是你的班级My违反了三原则;如果您编写自定义析构函数,那么您还应该编写自定义复制构造函数(和复制赋值运算符,但这在这里不相关)。

和:

struct My
{  My() { cout << "My constructor " << endl; }
   My(const My &) { cout << "My copy constructor " << endl; }
  ~My() { cout << "My destructor " << endl; }
};

MSVC 的输出是:

My constructor
My function
My copy constructor
My destructor
My destructor

如您所见,(复制)构造函数与析构函数正确匹配。

gcc 下的输出不变,因为 gcc 正在按照标准允许(但不是必需)执行复制省略。

于 2013-07-16T13:33:26.217 回答
3

您在这里缺少两件事:复制构造函数和 NRVO。

使用 MSVC++ 看到的行为是“正常”行为;my被创建并运行函数的其余部分;然后,在返回时,会创建对象的副本。本地my对象被销毁,并将副本返回给调用者,调用者只是将其丢弃,导致其销毁。

为什么您似乎缺少构造函数调用?因为编译器自动生成了一个复制构造函数,它被调用但不打印任何东西。如果您添加了自己的复制构造函数:

My(const My& Right) { cout << "My copy constructor " << endl; }

你会看到

My constructor      <----+
My function              |      this is the local "my" object
My copy constructor   <--|--+
My destructor       <----+  |   this is the return value
My destructor         <-----+

所以重点是:并不是对析构函数的调用比对构造函数的调用多,只是你没有看到对复制构造函数的调用。


在 gcc 输出中,您还看到应用了 NRVO。

NRVO(命名返回值优化)是允许编译器执行改变程序可见行为的优化的少数情况之一。实际上,允许编译器省略对临时返回值的拷贝,直接构造返回的对象,从而省略临时拷贝。

因此,没有创建副本,并且my实际上是返回的同一个对象。

My constructor  <-- called at the beginning of f
My function    
My destructor   <-- called after f is terminated, since
                    the caller discarded the return value of f
于 2013-07-16T13:34:09.563 回答
1

迄今为止,我一直在设计我的类,假设每个构造函数调用最多有一个析构函数调用 [...]

你仍然可以“假设”,因为它是真的。每个构造函数调用都将与一个析构函数调用同时进行。(请记住,如果您自己处理空闲/堆内存中的内容。)

[..] 并且可以依赖于编译器 [...]

在这种情况下,它不能。它依赖于优化。如果应用优化,MSVC 和 GCC 的行为相同。

为什么你看不到相同的行为?

1. 你不会跟踪你的对象发生的所有事情。编译器生成的函数会绕过您的输出。

如果你想“跟进”你的编译器对你的对象所做的事情,你应该定义所有的特殊成员,这样你就可以真正跟踪所有内容并且不会被任何隐式函数绕过。

struct My
{  
   My() { cout << "My constructor " << endl; }
   My(My const&) { cout << "My copy-constructor " << endl; }
   My(My &&) { cout << "My move-constructor " << endl; }
   My& operator=(My const&) { cout << "My copy-assignment " << endl; }
   My& operator=(My &&) { cout << "My move-assignment " << endl; }
  ~My() { cout << "My destructor " << endl; }
};

[注意:如果你有副本,移动构造函数和移动赋值将不会隐式出现,但很高兴看到编译器何时使用它们中的哪一个。]

2. 你没有在 MSVC 和 GCC 上进行优化编译。

如果使用 MSVC++11/O2选项编译,则输出为:

我的构造
函数 我的函数
我的析构函数

如果在调试模式下编译/没有优化:

我的构造
函数 我的函数
我的移动构造函数 我的
析构函数
我的析构函数

我无法对 gcc 进行测试以验证是否有一个选项可以强制执行所有这些步骤,但-O0我猜应该可以做到这一点。

这里优化和非优化编译有什么区别?

没有任何复制省略的情况:

此行中完全“未优化”的行为My my_in_main = function(); (更改名称以使事情清楚)将是:

  1. 称呼function()
  2. 在函数构造 MyMy my;
  3. 输出东西。
  4. 复制构造my到返回值实例中。
  5. 返回并销毁my实例。
  6. 复制(或在我的示例中移动)-将返回值实例构造为my_in_main.
  7. 销毁返回值实例。

如您所见:我们在这里最多有两个副本(或一个副本和一个移动),但编译器可能会忽略它们。

据我了解,即使没有打开优化(在这种情况下),第一个副本也会被省略,过程如下:

  1. 称呼function()
  2. 在函数中构造我的My my; 第一个构造函数输出!
  3. 输出东西。函数输出!
  4. 复制(或在我的示例中移动)-将返回值实例构造为my_in_main. 移动输出!
  5. 销毁返回值实例。破坏输出!

my_in_mainmain 结束时破坏,给出最后的Destroy 输出!. 所以我们现在知道在未优化的情况下会发生什么。

复制省略

可以省略副本(或移动,如果该类具有移动构造函数,如我的示例中所示)。

§ 12.8 [class.copy] / 31

当满足某些条件时,允许实现省略类对象的复制/移动构造,即使对象的复制/移动构造函数和/或析构函数具有副作用。

所以现在的问题是在这个例子中什么时候发生这种情况?第一个副本的 elison 的原因在同一段中给出:

[...] 在具有类返回类型的函数的 return 语句中,当表达式是具有与函数相同的 cvunqualified 类型的非易失性自动对象(函数或 catch 子句参数除外)的名称时返回类型,可以通过将自动对象直接构造到函数的返回值中来省略复制/移动操作。

返回类型匹配返回语句中的类型:functionMy my;直接构造成返回值。

第二次复制/移动的elison的原因:

[...] 当尚未绑定到引用 (12.2) 的临时类对象将被复制/移动到具有相同 cv 非限定类型的类对象时,可以通过构造临时类来省略复制/移动操作对象直接进入省略的复制/移动的目标。

目标类型匹配函数返回的类型:函数的返回值将被构造成my_in_main.

所以你在这里有一个级联:

My my;在您的函数中直接构造成直接构造成的返回值my_in_main所以实际上您在这里只有一个对象,并且实际上function()会(无论它做什么)对该对象进行操作my_in_main

  1. 称呼function()
  2. 在函数中将 My 实例构造成my_in_main. 构造函数输出!
  3. 输出东西。函数输出!

my_in_main在 main 结束时仍然被销毁,给出一个析构函数输出!.

这总共产生三个输出:如果打开优化,您观察到的输出。

一个不可能省略的例子。

在以下示例中,不能省略上述两个副本,因为类类型不匹配:

  • return 语句与返回类型不匹配
  • 目标类型与函数的返回类型不匹配

我刚刚创建了两种额外的类型:

#include <iostream>
using namespace std;
struct A
{  
   A(void) { cout << "A constructor " << endl; }
  ~A(void) { cout << "A destructor " << endl; }
};
struct B
{  
   B(A const&) { cout << "B copy from A" << endl; }
  ~B(void) { cout << "B destructor " << endl; }
};

struct C
{
   C(B const &) { cout << "C copy from B" << endl; }
  ~C(void) { cout << "C destructor " << endl; }
};

B function() { A my; cout << "function" << endl; return my; }

int main()
{ 
  C my_in_main(function());
  return 0;
}

在这里,我们有我上面提到的“完全未优化的行为”。我会参考我在那里画的点。

构造函数(参见 2.)
功能(见 3.)
B 从 A 复制(见 4.)
析构函数(参见 5.)
C 从 B 复制(见 6.)
B 析构函数(参见 7.)
C 析构函数(main 中的实例,main 结束时销毁)
于 2013-07-16T14:14:58.193 回答