为啥在按值返回时总是调用复制构造函数
Posted
技术标签:
【中文标题】为啥在按值返回时总是调用复制构造函数【英文标题】:why copy constructor is always called when return by value为什么在按值返回时总是调用复制构造函数 【发布时间】:2016-02-01 12:34:12 【问题描述】:当一个函数按值返回一个对象时,它会调用复制构造函数来创建一个临时对象(除非应用了 RVO)。该临时文件将在使用后被销毁,例如
MyClass function_return_by_value(MyClass par)
return par;
MyClass b;
MyClass a = function_return_by_value(b); // (1)
但是,如果根本不使用它,为什么我们需要创建这样一个临时文件呢?例如,为什么下面的代码没有被编译器“优化”以跳过临时创建和销毁?
MyClass b;
function_return_by_value(b); // (2)
在(1)中,返回值被分配给另一个变量,RVO 很可能适用。但是在(2)中,没有任何东西可以接收返回值,为什么没有进行优化呢?我已经尝试过 gcc 4.8.4 和 vc++ 2015,MyClass 的复制构造函数在两个编译器中都为 (2) 调用了两次。为什么编译器制造商都决定制作一个临时文件然后销毁它,即使临时文件根本没有使用?他们为什么不能避免这种情况?
【问题讨论】:
取决于您的复制构造函数中调用的副作用。 您需要将par
带到const MyClass&
以防止第二次复制。
谢谢@PSkocik,但我认为你应该先阅读我的问题。
@πάνταῥεῖ 可能的副作用是什么?谢谢
作为编译器,我猜你不会事先知道要调用的函数的所有上下文。因此,在翻译时,如果缺少更多信息,请假设最坏的情况。因此,即使对于 (2)。 (但不知道标准会说/允许什么)
【参考方案1】:
当 RVO 删除副本时,编译器可以删除副本的副作用(这本身就颇具争议),但是优化编译器不能简单地从程序中删除对象的整个存在,因为只要构建和/或破坏它有副作用。
这将允许以下程序不输出任何内容,这显然是错误的:
#include <iostream>
struct A
A() std::cout << "Booyah\n";
;
int main()
A a;
【讨论】:
【参考方案2】:我已经尝试过 gcc 4.8.4 和 vc++ 2015,MyClass 的复制构造函数在两个编译器中都为 (2) 调用了两次。
因为它应该是这样工作的!
真的,对于您希望对其进行优化的情况,C++ 中有引用机制;在你的情况下,它不能被优化掉,因为“我检查过”意味着你依赖于构造函数的副作用来向你展示它被调用了;编译器应该如何知道您在功能上不需要这种副作用?
【讨论】:
请注意,由于复制省略(包括 RVO/NRVO),无论如何都不能依赖构造函数和析构函数的副作用。在复制省略中描述的情况下,允许编译器不调用构造函数或析构函数,即使它们具有可观察到的副作用。【参考方案3】:您抱怨的副本不是从函数返回值到分配给它的变量的副本。它是创建返回值,因为您命令它是函数参数的副本。考虑下面的代码:
#include <iostream>
class Krowa
public:
Krowa() std::cout << "Default krowa\n";
Krowa(Krowa const &) std::cout << "Copied Krowa\n";
;
Krowa fun1(Krowa const & krowa)
return krowa;
Krowa fun2()
return Krowa;
int main()
fun1(Krowa);
fun2();
return 0;
输出是
默认krowa 复制的 Krowa 默认krowa
fun1
返回值是参数的副本,所以调用了复制构造函数。 fun2
返回值是一个默认构造对象,所以调用了默认constructr。
【讨论】:
【参考方案4】:构造对象的函数和不构造对象的函数是两个不同的函数。当编译器编译一个函数时,它不知道该函数将如何被调用。它怎么知道它应该编译哪个版本?一个会构建还是一个不会?
现在,让我们假设编译器确实知道在同一个翻译单元中,从不使用返回值。可以想象,编译器然后可以优化函数以从不构造对象。但是,如果稍后在另一个编译单元中调用该函数并使用结果 怎么办?该函数的优化版本将不起作用,因为它不返回任何内容!
好的,如果编译器总是编译所有返回值的函数的两个版本,并且在使用返回值时选择一个,而在不使用返回值时选择另一个。好吧,编译器仍然不允许这样做,因为对象的构造函数和析构函数可能有副作用,并且这些副作用不能简单地消失(复制省略允许这样做,但这是一种特殊情况,不适用于此处)。
好的,假设编译函数时对象的构造函数和析构函数是可见的。然后,如果编译器可以证明没有副作用,那么理论上它可以应用上面讨论的优化。鉴于这种优化发生的严格要求(可见 c'tor 和 d'tor 没有副作用)和不利影响(所有返回值函数的两个版本会增加编译时间并可能增加二进制大小),我对此表示怀疑已经认真考虑过优化类型。
不过,您可以手动进行此优化。简单的把构造和返回对象的函数部分和有副作用的函数部分分开:
void function_that_has_side_effects(const MyClass& par):
MyClass function_return_by_value(MyClass par)
function_that_has_side_effects(par);
return par;
现在,如果您想要副作用,但不需要返回值,只需调用 function_that_has_side_effects
。
MyClass b;
function_that_has_side_effects(b); // (2)
【讨论】:
以上是关于为啥在按值返回时总是调用复制构造函数的主要内容,如果未能解决你的问题,请参考以下文章