我正在尝试确定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输出更接近我的预期。
到目前为止,我一直在设计我的类,假设对于每个构造函数调用,最多只有一个析构函数调用,但是这个例子似乎表明这个假设并不总是成立,并且可以依赖于编译器。标准中是否有任何内容指定此处应该发生什么,或者最好避免函数返回非平凡对象?如果这个问题重复,请道歉。
答案 0 :(得分:7)
在这两种情况下,编译器都会为您生成一个复制构造函数,它没有输出,因此您不知道它是否被调用:See this question.
在第一种情况下,使用编译器生成的复制构造函数,它与第二个析构函数调用匹配。行return my;
调用复制构造函数,为其提供用于构造返回值的变量my
。这不会产生任何输出。
my
然后被销毁。函数调用完成后,返回值将在行{ function();
的末尾被销毁。
在第二种情况下,返回的副本完全为elided(允许编译器执行此操作作为优化)。您只有一个My
实例。 (是的,即使它改变了程序的可观察行为,也允许这样做!)
这些都没关系。虽然作为一般规则,如果你定义自己的构造函数和析构函数,你还应该定义自己的复制构造函数(和赋值运算符,如果你有c ++ 11,可能会移动构造函数和移动赋值)。
尝试添加自己的复制构造函数,看看你得到了什么。像
这样的东西My (const My& otherMy) { cout << "My copy constructor\n"; }
答案 1 :(得分:3)
问题是您的班级My
违反了Rule of Three;如果你编写一个自定义析构函数,那么你也应该编写一个自定义复制构造函数(和复制赋值运算符,但这里不相关)。
使用:
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正在执行标准所允许的复制省略(但不是必需的)。
答案 2 :(得分: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 <-----+
所以重点是:不是对构造函数的析构函数有更多的调用,只是你没有看到对 copy 构造函数的调用。
在gcc输出中,您也看到了应用NRVO。
NRVO(命名为Return Value Optimization)是允许编译器执行改变程序可见行为的优化的少数情况之一。实际上,允许编译器将副本删除到临时返回值,并直接构造返回的对象,从而删除临时副本。
因此,不会创建副本,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