以下代码让我感到困惑:
#include <iostream>
using namespace std;
class B {
public:
B() {
cout << "constructor\n";
}
B(const B& rhs) {
cout << "copy ctor\n";
}
B & operator=(const B & rhs) {
cout << "assignment\n";
}
~B() {
cout << "destructed\n";
}
B(int i) : data(i) {
cout << "constructed by parameter " << data << endl;
}
private:
int data;
};
B play(B b)
{
return b;
}
int main(int argc, char *argv[])
{
#if 1
B t1;
t1 = play(5);
#endif
#if 0
B t1 = play(5);
#endif
return 0;
}
环境是Fedora 15上的g ++ 4.6.0。 第一个代码片段输出如下:
constructor
constructed by parameter 5
copy ctor
assignment
destructed
destructed
destructed
第二个片段代码输出是:
constructed by parameter 5
copy ctor
destructed
destructed
为什么在第一个例子中调用了三个析构函数,而在第二个例子中它只有两个?
答案 0 :(得分:2)
第一种情况:
B t1;
t1 = play(5);
t1
的默认构造函数创建对象B
。 play()
,请使用B
创建B(int i)
的临时对象。 5
作为B
的对象创建,并且play()
被调用。return b;
在play()
内导致copy constructor
被调用以返回对象的副本。
t1 =
调用 Assignemnt运算符将返回的对象副本分配给t1
。#3
中创建的临时对象。 #2
中销毁返回的临时对象。 t1
。第二种情况:
B t1 = play(5);
B
的参数化构造函数创建类B
的临时对象,该构造函数将int
作为参数。 B
的复制构造函数。 #1
。t1
。 在第二种情况下,一个析构函数调用较少,因为在第二种情况下,编译器使用 Return value Optimization 并在从play()
返回时忽略调用以创建其他临时对象。相反,Base
对象是在临时分配的位置创建的。
答案 1 :(得分:1)
首先,检查子表达式play(5)
。这两种情况下的表达方式相同。
在函数调用表达式中,每个参数都从其参数(ISO / IEC 14882:2003 5.2.2 / 4)进行复制初始化。在这种情况下,这涉及通过使用非显式构造函数将5
转换为B
来创建临时int
,然后使用复制构造函数进行初始化参数B
。但是,允许实现通过使用12.8中指定的规则从b
使用转换构造函数直接初始化b
来消除临时。
int
的类型为play(5)
,并且 - 作为返回非引用的函数 - 它是 rvalue 。
B
语句隐式地将返回表达式转换为返回值的类型(6.6.3),然后使用转换后的表达式复制初始化(8.5 / 12)返回对象。
在这种情况下,返回表达式的类型已经正确,因此不需要转换,但仍需要复制初始化。
除了返回值优化
命名返回值优化(NRVO)指的是如果形式return
,其中return x;
是函数本地的自动对象,则返回语句的情况。发生时,允许实现在返回值的位置构造x
,并在x
点消除复制初始化。
虽然标准中没有这样命名,但NRVO通常是指12.8 / 15中描述的第一种情况。
return
中无法进行此特定优化,因为play
不是函数体的本地对象,它是在输入函数时已经构造的参数的名称
(未命名)返回值优化(RVO)对它所引用的内容的协议更少,但通常用于表示返回表达式不是命名对象但转换为返回类型的表达式的情况并且可以组合返回对象的复制初始化,以便直接从转换结果中初始化返回对象,从而消除一个临时对象。
RVO不适用于b
,因为play
已经是b
类型,因此复制初始化等同于 direct-初始化,不需要临时对象。
在这两种情况下,B
都需要使用play(5)
构造B
参数,并将B(int)
复制初始化为返回对象。它也可以在参数的初始化中使用第二个副本,但是即使未明确请求优化,许多编译器也会删除此副本。这些对象中的两个(或全部)都是临时对象。
在表达式语句B
中,将调用复制赋值运算符以将t1 = play(5);
的返回值的值复制到play
和两个临时值(参数和返回值{ {1}})将被销毁。当然t1
必须在此语句之前构造,并且它的析构函数将在其生命周期结束时被调用。
在声明语句play
中,逻辑t1
初始化为play的返回值,并且将使用完全相同数量的临时值作为表达式语句B t1 = play(5);
。但是,这是12.8 / 15中涵盖的第二种情况,其中允许实现消除用于t1
的返回值的临时值,而是允许返回对象为别名t1 = play(5);
。 play
函数的运行方式完全相同,但因为返回对象只是t1
的别名,其返回语句实际上直接初始化play
并且没有单独的临时对象用于返回需要销毁的价值。
答案 2 :(得分:0)
第一个片段构造三个对象:
这是我的猜测,虽然看起来效率低下。
答案 3 :(得分:0)
请参阅Als发布的第一个场景的播放内容。
我认为(编辑:错误;见下文)与第二种情况的区别在于编译器足够聪明,可以使用NRVO(命名返回值优化)并忽略中间副本:而不是在返回时创建临时副本(从play开始),编译器使用play函数内部的实际“b”作为t1复制构造函数的rvalue。
Dave Abrahams在article上有一个return value optimization副本,而这里是维基百科。
编辑:实际上,Als也增加了第二种情景的逐个播放。 :)进一步修改:实际上,我上面的说法不正确。在任何一种情况下都没有使用NRVO,因为根据接受的答案for this question,标准禁止直接从函数参数(播放中的b)复制到函数的返回值位置(至少没有内联)。 强>
即使允许使用NRVO,我们也可以告诉它至少在第一种情况下没有使用它:如果是,第一种情况不会涉及复制构造函数。第一种情况下的复制构造函数来自隐藏的副本,从命名值b(在播放函数中)到隐藏的返回值位置进行播放。第一种情况不涉及明确的复制结构,因此这是唯一可以出现的地方。
实际上是这样的:在任何一种情况下都没有发生NRVO,并且在返回时正在创建隐藏副本......但在第二种情况下,编译器能够直接在t1的位置构造隐藏的返回副本。因此,从b到返回值的副本没有被省略,但从返回值到t1的副本是。但是,对于已经构造了t1的第一种情况,编译器更难以进行此优化(读取:它没有这样做;))。如果t1已经在与返回值的位置不兼容的地址构造,则编译器无法直接使用t1的地址作为隐藏的返回值副本。
答案 4 :(得分:0)
在你的第一个例子中,你正在调用三个构造函数:
声明B()
时的B t1;
构造函数,如果B()
是公共的,也是一个定义。换句话说,编译器将尝试将任何声明的对象初始化为某个基本有效状态,并将B()
视为将B
大小的内存块转换为所述基本有效状态的方法,以便调用t1
的方法不会破坏程序。
B(int)
构造函数,用作隐式转换; play()
获取B但获得了int,但B(int)
被视为将int
转换为B
的方法。
B(const B& rhs)
复制构造函数,它将B
返回的play()
的值复制到一个临时值中,这样它的作用域就足够长,可以继续使用在任务操作员中。
当范围退出时,上述每个构造函数都必须与析构函数匹配。
但是,在第二个示例中,您正在使用t1
的结果显式初始化play()
的值,因此编译器不需要浪费为{{1}提供基本状态的循环在它将t1
的结果副本分配给新变量之前。所以你只能打电话
play()
为B(int)
play(B)
以便B(const B& rhs)
初始化(无论您的复制构造函数决定了什么)t1
的结果的正确副本。
在这种情况下,您没有看到第三个构造函数,因为编译器将play()
的返回值“删除”到play()
;也就是说,它知道在t1
返回之前t1
在有效状态中不存在,所以它只是将返回值直接写入为play()
预留的内存中。