类的初始化过程中会发生什么?
这是令我困惑的代码:
#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
为什么第一个示例中调用了三个析构函数,而第二个示例中只调用了两个?
Here is the code which confuses me:
#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;
}
Environment is g++ 4.6.0 on Fedora 15.
The first code fragment output is as follows:
constructor
constructed by parameter 5
copy ctor
assignment
destructed
destructed
destructed
And the second fragment code output is:
constructed by parameter 5
copy ctor
destructed
destructed
Why are are three destructors are called in the first example, while in the second it is only two?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
第一种情况:
B
的默认构造函数创建对象t1
。play()
,使用B(int i)
创建了B
的临时对象。5
作为 an 传递,并创建B
对象,并调用play()
。play()
内的return b;
会导致调用复制构造函数
来返回对象的副本。t1 =
调用Assignemnt 运算符,将返回的对象副本分配给t1
。#3
中创建的临时对象。#2
中返回的临时对象。t1
。第二种情况:
B
的临时对象是通过调用B
的参数化构造函数创建的,该构造函数将int
作为参数参数。B
的复制构造函数。#1
中创建的临时对象。t1
。在第二种情况下,一次析构函数调用较少,因为在第二种情况下,编译器使用返回值优化 并在从
play()
返回时省略创建额外临时对象的调用。相反,Base
对象是在分配临时对象的位置创建的。First Case:
t1
by calling default constructor ofB
.play()
, A temporary object ofB
is created by usingB(int i)
.5
is passed as an and object ofB
is created, andplay()
is called.return b;
insideplay()
causes thecopy constructor
to be called for returning a copy of object.t1 =
calls the Assignemnt operator to assign the returned object copy tot1
.#3
.#2
.t1
.Second case:
B
is created by calling parameterized constructor ofB
which takesint
as a paraemter.B
.#1
.t1
.One destructor call is less in Second Case because, in second Case the compiler uses Return value Optimization and elides the call to create an additional temporary object while returning from
play()
. Instead theBase
object is created in the location where the temporary would have been assigned.首先,检查子表达式
play(5)
。该表达式在两种情况下都是相同的。在函数调用表达式中,每个参数都是从其参数复制初始化的 (ISO/IEC 14882:2003 5.2.2/4)。在本例中,这涉及到使用非显式构造函数将
5
转换为B
,该构造函数采用int
创建临时B< /code> 然后使用复制构造函数初始化参数
b
。但是,根据 12.8 中指定的规则,允许实现通过使用int
的转换构造函数直接初始化b
来消除临时值。play(5)
的类型是B
并且 - 作为返回非引用的函数 - 它是一个右值。return
语句将返回表达式隐式转换为返回值的类型 (6.6.3),然后使用转换后的表达式复制初始化 (8.5/12) 返回对象。在这种情况下,返回表达式已经是正确的类型,因此不需要转换,但仍然需要复制初始化。
除了返回值优化
命名返回值优化(NRVO)是指返回语句为 if 形式
return x;
wherex
是函数本地的自动对象。当发生时,允许实现在返回值的位置构造x
,并消除返回
处的复制初始化。尽管标准中没有这样命名,但 NRVO 通常指的是 12.8/15 中描述的第一种情况。
这种特殊的优化在
play
中是不可能的,因为b
不是函数体的本地对象,它是在调用时已经构造的参数的名称。输入函数。(未命名的)返回值优化(RVO)对其所指的含义甚至不太一致,但通常用于指返回表达式不是命名对象而是转换为返回类型和 复制初始化可以组合起来,以便直接根据消除一个临时对象的转换结果来初始化返回对象。
RVO 不适用于
play
,因为b
已经是B
类型,因此复制初始化是等效的直接初始化并且不需要临时对象。在这两种情况下,
play(5)
都需要使用B(int)
作为参数构造B
并复制初始化B
到返回对象。它还可以在参数的初始化中使用第二个副本,但即使没有明确请求优化,许多编译器也会消除此副本。这些对象(或全部)都是临时对象。表达式语句
t1 = play(5);
中会调用复制赋值运算符,将play
的返回值复制到t1
并且两个临时变量(play
的参数和返回值)将被销毁。当然,t1
必须在此语句之前构建,并且其析构函数将在其生命周期结束时被调用。在声明语句
B t1 = play(5);
中,逻辑上t1
使用 play 的返回值进行初始化,并且将使用与表达式完全相同数量的临时变量语句t1 = play(5);
。然而,这是 12.8/15 中介绍的第二种情况,其中允许实现消除用于play
返回值的临时对象,而是允许返回对象别名t1< /代码>。
play
函数的操作方式完全相同,但因为它的返回对象只是t1
的别名,所以它的 return 语句有效地直接初始化t1
并没有单独的临时对象用于需要销毁的返回值。First, examine the sub-expression
play(5)
. This expression is the same in both cases.In a function call expression each parameter is copy-initialized from its argument (ISO/IEC 14882:2003 5.2.2/4). In this case this involves converting
5
to aB
by using the non-explicit constructor taking anint
to create a temporaryB
and then using the copy-constructor to initialize the parameterb
. However, the implementation is permitted to eliminate the temporary by directly initializingb
using the converting constructor fromint
under the rules specified in 12.8.The type of
play(5)
isB
and - as function returning a non-reference - it is an rvalue.The
return
statement implicitly converts the return expression to the type of the return value (6.6.3) and then copy-initializes (8.5/12) the return object with the converted expression.In this case the return expression is already of the correct type, so no conversion is required but the copy initialization is still required.
Aside on return value optimizations
The named return value optimization (NRVO) refers to the situation where the return statement is if the form
return x;
wherex
is an automatic object local to the function. When occurs the implementation is allowed to constructx
in the location for the return value and eliminate the copy-initialization at the point ofreturn
.Although it is not named as such in the standard, NRVO usually refers to the first situation described in 12.8/15.
This particular optimization is not possible in
play
becauseb
is not an object local to the function body, it is the name of the parameter which has already been constructed by the time the function is entered.The (unnamed) return value optimization (RVO) has even less agreement on what it refers to but is usually used to refer to the situation where the return expression is not a named object but an expression where the conversion to the return type and copy-initialization of the return object can be combined so that the return object is initialized straight from the result of the conversion eliminating one temporary object.
The RVO doesn't apply in
play
becauseb
is already of typeB
so the copy-initialization is equivalent to direct-initialization and no temporary object is necessary.In both cases
play(5)
requires the construction of aB
usingB(int)
for the parameter and a copy-initialization ofB
to the return object. It may also use a second copy in the initialization of the parameter but many compilers eliminate this copy even when optimizations are not explicitly requested. Both (or all) of these objects are temporaries.In the expression statement
t1 = play(5);
the copy assignment operator will be called to copy the value of the return value ofplay
tot1
and the two temporaries (parameter and return value ofplay
) will be destroyed. Naturallyt1
must have been constructed prior to this statement and its destructor will be called at the end of its lifetime.In the declaration statement
B t1 = play(5);
, logicallyt1
is initialized with the return value of play and exactly the same number of temporaries will be used as the expression statementt1 = play(5);
. However, this is the second of the situations covered in 12.8/15 where the implementation is allowed to eliminate the temporary used for the return value ofplay
and instead allow the return object to aliast1
. Theplay
function operates in exactly the same way but because it the return object is just an alias tot1
its return statement effectively directly initializest1
and there is no separate temporary object for the return value that needs to be destroyed.第一个片段构造了三个对象:
这是我的猜测,尽管它看起来效率低下。
The first fragment constructs three objects:
This is my guess, although it looks inefficient.
请参阅 Als 发布的第一个场景的详细情况。
我认为(编辑:错误;见下文)与第二种情况的区别在于编译器足够聪明,可以使用 NRVO(名为返回值优化)并删除中间副本:而不是在返回时创建临时副本(从播放中) ),编译器使用 play 函数内部的实际“b”作为 t1 复制构造函数的右值。
Dave Abrahams 有一篇文章副本省略,这里是维基百科上的 返回值优化。
编辑:实际上,阿尔斯也添加了第二个场景的详细剧情。 :)
进一步编辑:实际上,我上面错了。在这两种情况下都不会使用 NRVO,因为根据接受的答案 为此问题。
即使 NRVO 被允许,我们也可以知道它至少在第一种情况下没有被使用:如果是的话,第一种情况就不会涉及任何复制构造函数。第一种情况中的复制构造函数来自从命名值 b(在 play 函数中)到 play 的隐藏返回值位置的隐藏副本。第一种情况不涉及显式的复制构造,因此这是它唯一可能出现的地方。
实际情况是这样的:在这两种情况下都不会发生 NRVO,并且在返回时创建隐藏副本...但在第二种情况下,编译器能够直接在 t1 的位置构造隐藏的返回副本。因此,从 b 到返回值的复制没有被省略,但从返回值到 t1 的复制被省略。然而,对于第一种情况,t1 已经被构造,编译器很难进行这种优化(阅读:它没有这样做;))。如果 t1 已在与返回值位置不兼容的地址处构造,则编译器无法直接将 t1 的地址用于隐藏的返回值副本。
Refer to what Als posted for a play-by-play of the first scenario.
I think (EDIT: wrongly; see below) the difference with the second case is that the compiler was smart enough to use the NRVO (named return value optimization) and elide the middle copy: Instead of creating a temporary copy on return (from play), the compiler used the actual "b" inside of the play function as the rvalue for t1's copy constructor.
Dave Abrahams has an article on copy elision, and here's Wikipedia on the return value optimization.
EDIT: Actually, Als added a play-by-play of the second scenario, too. :)
Further edits: Actually, I was incorrect above. The NRVO is not being used in either case, because the standard forbids eliding copies directly from function arguments (b in play) to the return value location of a function (at least without inlining), according to the accepted answer for this question.
Even if the NRVO were allowed, we can tell that it's not being used in the first case at least: If it were, the first case would not involve a copy constructor whatsoever. The copy constructor in the first case comes from the hidden copy from the named value b (in the play function) to the hidden return value location for play. The first case involves no explicit copy construction, so that is the only place where it can arise.
What's actually going on is this: NRVO is not occurring in either case, and a hidden copy is being created on return...but in the second case, the compiler was able to construct the hidden return copy directly at t1's location. So, the copy from b to the return value was not elided, but the copy from the return value to t1 was. However, the compiler had a harder time doing this optimization for the first case where t1 was already constructed (read: it didn't do it ;)). If t1 is already constructed at an address incompatible with the return value's location, the compiler isn't able to use t1's address directly for the hidden return value copy.
在第一个示例中,您调用三个构造函数:
声明
B t1;
时的B()
构造函数,这也是一个定义 ifB()
是公开的。换句话说,编译器将尝试将任何声明的对象初始化为某种基本的有效状态,并将B()
视为转换B
大小的内存块的方法进入所述基本有效状态,以便在t1
上调用的方法不会破坏程序。B(int)
构造函数,用作隐式转换;play()
接受 B 但被赋予 int,但B(int)
被视为将int
转换为B 的方法
.B(const B& rhs)
复制构造函数,它将把play()
返回的B
的值复制到临时值,这样它就有足够长的作用域,可以在赋值运算符中使用。当作用域退出时,上述每个构造函数都必须与析构函数相匹配。
但是,在第二个示例中,您使用 play() 的结果显式初始化 t1 的值,因此编译器不需要浪费周期来提供基本的在将
play()
结果的副本分配给新变量之前,先将状态设置为t1
。因此,您只需调用t1
初始化为(无论什么)您的复制构造函数决定是)的正确副本play()
的结果。在这种情况下,您看不到第三个构造函数,因为编译器将
play()
的返回值“省略”到t1
中;也就是说,它知道在play()
返回之前t1
并不以有效状态存在,因此它只是将返回值直接写入为预留的内存中>t1
。In your first example, you're calling three constructors:
The
B()
constructor when you declareB t1;
, which is also a definition ifB()
is public. In other words, the compiler will try to initialize any declared objects to some basic valid state, and treatsB()
as the method for transforming aB
-sized block of memory into said basic valid state, so that methods called ont1
won't break the program.The
B(int)
constructor, used as an implicit conversion;play()
takes a B but was given an int, butB(int)
is considered a method for convertingint
toB
.The
B(const B& rhs)
copy constructor, which will copy the value of theB
returned byplay()
into a temporary value so that it will have scope long enough to survive being used in an assignment operator.Each of the above constructors must be matched with a destructor when the scope exits.
In your second example, however, your are explicitly initializing the value of
t1
with the result ofplay()
, so the compiler doesn't need to waste cycles providing a basic state tot1
before it assigns a copy ofplay()
's result to the new variable. So you only callB(int)
to get a useful argument forplay(B)
B(const B& rhs)
so thatt1
will be initialized with (whatever your copy constructor decides is) a proper copy ofplay()
's results.You don't see a third constructor in this case because the compiler is "eliding" the returned value of
play()
intot1
; that is, it knew thatt1
did not exist in a valid state beforeplay()
returns, so it's just writing the return value directly into the memory set aside fort1
.