复制初始化和直接初始化有区别吗?
假设我有这个函数:
void my_test()
{
A a1 = A_factory_func();
A a2(A_factory_func());
double b1 = 0.5;
double b2(0.5);
A c1;
A c2 = A();
A c3(A());
}
在每个分组中,这些语句是否相同? 或者在某些初始化中是否有额外的(可能是可优化的)副本?
我见过人们说这两件事。 请引用文字作为证据。 还请添加其他案例。
Suppose I have this function:
void my_test()
{
A a1 = A_factory_func();
A a2(A_factory_func());
double b1 = 0.5;
double b2(0.5);
A c1;
A c2 = A();
A c3(A());
}
In each grouping, are these statements identical? Or is there an extra (possibly optimizable) copy in some of the initializations?
I have seen people say both things. Please cite text as proof. Also add other cases please.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(9)
C++17 更新
在 C++17 中,
A_factory_func()
的含义从创建临时对象 (C++<=14) 更改为仅指定此表达式所属的任何对象的初始化在 C++17 中初始化为(宽松地说)。 这些对象(称为“结果对象”)是由声明创建的变量(例如a1
)、初始化最终被丢弃时创建的人工对象,或者引用绑定需要对象(例如,在A_factory_func();
中,最后一种情况是人为创建的,称为“临时物化”,因为A_factory_func()
没有变量或引用。否则需要一个对象存在)。作为我们案例中的示例,在
a1
和a2
的情况下,特殊规则规定,在此类声明中,纯右值初始值设定项的结果对象与类型相同a1
是变量a1
,因此A_factory_func()
直接初始化对象a1
。 任何中间函数式类型转换都不会产生任何效果,因为A_factory_func(another-prvalue)
只是“传递”外部纯右值的结果对象,使其成为内部纯右值的结果对象。取决于
A_factory_func()
返回的类型。 我假设它返回一个A
- 那么它会做同样的事情 - 只不过当复制构造函数是显式的时,第一个将会失败。 阅读 8.6/14这是做同样的事情,因为它是一个内置类型(这意味着这里不是类类型)。 阅读8.6/14。
这不是做同样的事情。 如果
A
是非 POD,则第一个默认初始化,并且不会对 POD 进行任何初始化(请阅读 8.6/9)。 第二个副本初始化:值初始化一个临时值,然后将该值复制到c2
(阅读 5.2.3/2 和 8.6/ 14)。 这当然需要一个非显式的复制构造函数(阅读8.6/14和 12.3.1/3 和 13.3.1.3/1 )。 第三个为函数c3
创建函数声明,该函数返回A
并接受一个指向返回A
的函数的函数指针(读取 < a href="http://eel.is/c++draft/dcl.ambig.res" rel="noreferrer">8.2)。深入研究初始化 直接初始化和复制初始化
虽然它们看起来相同并且应该执行相同的操作,但这两种形式在某些情况下却有显着不同。 初始化的两种形式是直接初始化和复制初始化:
我们可以将它们归因于每种形式:
T< 的构造函数/code>(包括
显式
),参数为x
。 重载解析将找到最佳匹配的构造函数,并在需要时执行所需的任何隐式转换。x
转换为T
类型的对象。 (然后它可能会将该对象复制到要初始化的对象中,因此也需要复制构造函数 - 但这在下面并不重要)如您所见,复制初始化在某种程度上是关于可能的隐式转换的直接初始化:虽然直接初始化具有所有可供调用的构造函数,并且此外可以执行匹配参数类型所需的任何隐式转换,但复制初始化只能设置一个隐式转换转换序列。
我努力尝试得到以下代码为每种表单输出不同的文本,没有通过
显式
构造函数使用“明显”。它是如何工作的,为什么会输出这个结果?
直接初始化
它首先对转换一无所知。 它只会尝试调用构造函数。 在这种情况下,以下构造函数可用并且完全匹配:
调用该构造函数不需要任何转换,更不用说用户定义的转换(请注意,这里也没有发生 const 限定转换)。 所以直接初始化就会调用它。
复制初始化
如上所述,当
a
没有类型B
或从它派生时(这里显然是这种情况),复制初始化将构造一个转换序列。 因此它将寻找进行转换的方法,并会找到以下候选者请注意我如何重写转换函数:参数类型反映了
this
指针的类型,在非常量成员函数中是非常量的。 现在,我们用 x 作为参数来调用这些候选者。 获胜者是转换函数:因为如果我们有两个候选函数都接受对同一类型的引用,那么 less const 版本会获胜(顺便说一句,这也是更喜欢非 const 的机制) -const 成员函数调用非常量对象)。请注意,如果我们将转换函数更改为 const 成员函数,则转换是不明确的(因为两者的参数类型均为
A const&
):Comeau 编译器会正确拒绝它,但 GCC 以非迂腐模式接受它。 不过,切换到-pedantic
也会使其输出正确的歧义警告。C++17 Update
In C++17, the meaning of
A_factory_func()
changed from creating a temporary object (C++<=14) to just specifying the initialization of whatever object this expression is initialized to (loosely speaking) in C++17. These objects (called "result objects") are the variables created by a declaration (likea1
), artificial objects created when the initialization ends up being discarded, or if an object is needed for reference binding (like, inA_factory_func();
. In the last case, an object is artificially created, called "temporary materialization", becauseA_factory_func()
doesn't have a variable or reference that otherwise would require an object to exist).As examples in our case, in the case of
a1
anda2
special rules say that in such declarations, the result object of a prvalue initializer of the same type asa1
is variablea1
, and thereforeA_factory_func()
directly initializes the objecta1
. Any intermediary functional-style cast would not have any effect, becauseA_factory_func(another-prvalue)
just "passes through" the result object of the outer prvalue to be also the result object of the inner prvalue.Depends on what type
A_factory_func()
returns. I assume it returns anA
- then it's doing the same - except that when the copy constructor is explicit, then the first one will fail. Read 8.6/14This is doing the same because it's a built-in type (this means not a class type here). Read 8.6/14.
This is not doing the same. The first default-initializes if
A
is a non-POD, and doesn't do any initialization for a POD (Read 8.6/9). The second copy initializes: Value-initializes a temporary and then copies that value intoc2
(Read 5.2.3/2 and 8.6/14). This of course will require a non-explicit copy constructor (Read 8.6/14 and 12.3.1/3 and 13.3.1.3/1 ). The third creates a function declaration for a functionc3
that returns anA
and that takes a function pointer to a function returning aA
(Read 8.2).Delving into Initializations Direct and Copy initialization
While they look identical and are supposed to do the same, these two forms are remarkably different in certain cases. The two forms of initialization are direct and copy initialization:
There is behavior we can attribute to each of them:
T
(includingexplicit
ones), and the argument isx
. Overload resolution will find the best matching constructor, and when needed will do any implicit conversion required.x
to an object of typeT
. (It then may copy over that object into the to-initialized object, so a copy constructor is needed too - but this is not important below)As you see, copy initialization is in some way a part of direct initialization with regard to possible implicit conversions: While direct initialization has all constructors available to call, and in addition can do any implicit conversion it needs to match up argument types, copy initialization can just set up one implicit conversion sequence.
I tried hard and got the following code to output different text for each of those forms, without using the "obvious" through
explicit
constructors.How does it work, and why does it output that result?
Direct initialization
It first doesn't know anything about conversion. It will just try to call a constructor. In this case, the following constructor is available and is an exact match:
There is no conversion, much less a user defined conversion, needed to call that constructor (note that no const qualification conversion happens here either). And so direct initialization will call it.
Copy initialization
As said above, copy initialization will construct a conversion sequence when
a
has not typeB
or derived from it (which is clearly the case here). So it will look for ways to do the conversion, and will find the following candidatesNotice how I rewrote the conversion function: The parameter type reflects the type of the
this
pointer, which in a non-const member function is to non-const. Now, we call these candidates withx
as argument. The winner is the conversion function: Because if we have two candidate functions both accepting a reference to the same type, then the less const version wins (this is, by the way, also the mechanism that prefers non-const member function calls for non-const objects).Note that if we change the conversion function to be a const member function, then the conversion is ambiguous (because both have a parameter type of
A const&
then): The Comeau compiler rejects it properly, but GCC accepts it in non-pedantic mode. Switching to-pedantic
makes it output the proper ambiguity warning too, though.赋值与初始化不同。
以下两行都会进行初始化。 完成了单个构造函数调用:
但它不等于:
我目前没有文本来证明这一点,但很容易进行实验:
Assignment is different from initialization.
Both of the following lines do initialization. A single constructor call is done:
but it's not equivalent to:
I don't have a text at the moment to prove this but it's very easy to experiment:
double b1 = 0.5;
是构造函数的隐式调用。double b2(0.5);
是显式调用。查看以下代码以了解差异:
如果您的类没有显式构造函数,则显式调用和隐式调用是相同的。
double b1 = 0.5;
is implicit call of constructor.double b2(0.5);
is explicit call.Look at the following code to see the difference:
If your class has no explicit constuctors than explicit and implicit calls are identical.
当你初始化一个对象时,你可以看到它在
显式
和隐式
构造函数类型中的区别:类:
并且在
main
function :默认情况下,构造函数是
隐式
,因此您有两种方法来初始化它:以及将结构定义为
显式
只是你有一种直接的方式:You can see its difference in
explicit
andimplicit
constructor types when you initialize an object :Classes :
And in the
main
function :By default, a constructor is as
implicit
so you have two way to initialize it :And by defining a structure as
explicit
just you have one way as direct :值得注意的是:
[12.2/1]
类类型的临时对象是在各种上下文中创建的:...以及在某些初始化 (8.5) 中。
即,用于复制初始化。
[12.8/15]
当满足某些条件时,允许实现省略类对象的复制构造...
换句话说,一个好的编译器不会当可以避免时,创建一个用于复制初始化的副本; 相反,它只会直接调用构造函数——即,就像直接初始化一样。
换句话说,在大多数情况下,复制初始化就像直接初始化一样编写了可理解的代码的地方。 由于直接初始化可能会导致任意(因此可能未知)转换,因此我更喜欢尽可能使用复制初始化。 (好处是它实际上看起来像初始化。)
技术精湛:
[12.2/1 续上]
即使避免创建临时对象 (12.8),也必须遵守所有语义限制,就像创建临时对象一样。
很高兴我没有写C++ 编译器。
Of note:
[12.2/1]
Temporaries of class type are created in various contexts: ... and in some initializations (8.5).
I.e., for copy-initialization.
[12.8/15]
When certain criteria are met, an implementation is allowed to omit the copy construction of a class object ...
In other words, a good compiler will not create a copy for copy-initialization when it can be avoided; instead it will just call the constructor directly -- ie, just like for direct-initialization.
In other words, copy-initialization is just like direct-initialization in most cases <opinion> where understandable code has been written. Since direct-initialization potentially causes arbitrary (and therefore probably unknown) conversions, I prefer to always use copy-initialization when possible. (With the bonus that it actually looks like initialization.)</opinion>
Technical goriness:
[12.2/1 cont from above]
Even when the creation of the temporary object is avoided (12.8), all the semantic restrictions must be respected as if the temporary object was created.
Glad I'm not writing a C++ compiler.
第一个分组:取决于
A_factory_func
返回的内容。 第一行是复制初始化的示例,第二行是直接初始化。 如果A_factory_func
返回一个A
对象,那么它们是等价的,它们都调用A
的复制构造函数,否则第一个版本创建类型的右值A
来自A_factory_func
返回类型的可用转换运算符或适当的A
构造函数,然后调用复制构造函数来构造a1
从这个临时的。 第二个版本尝试找到一个合适的构造函数,该构造函数接受 A_factory_func 返回的任何内容,或者接受返回值可以隐式转换为的内容。第二组:完全相同的逻辑,除了内置类型没有任何外来构造函数,因此实际上它们是相同的。
第三组:
c1
默认初始化,c2
从临时初始化的值复制初始化。 如果用户提供的默认构造函数(如果有)没有显式初始化它们,则具有 pod 类型的 c1 的任何成员(或成员的成员等)可能不会被初始化。 对于c2
,这取决于是否存在用户提供的复制构造函数以及是否适当地初始化这些成员,但临时的成员都将被初始化(如果没有以其他方式显式初始化,则初始化为零)。 正如 litb 所发现的,c3
是一个陷阱。 它实际上是一个函数声明。First grouping: it depends on what
A_factory_func
returns. The first line is an example of copy initialization, the second line is direct initialization. IfA_factory_func
returns anA
object then they are equivalent, they both call the copy constructor forA
, otherwise the first version creates an rvalue of typeA
from an available conversion operators for the return type ofA_factory_func
or appropriateA
constructors, and then calls the copy constructor to constructa1
from this temporary. The second version attempts to find a suitable constructor that takes whateverA_factory_func
returns, or that takes something that the return value can be implicitly converted to.Second grouping: exactly the same logic holds, except that built in types don't have any exotic constructors so they are, in practice, identical.
Third grouping:
c1
is default initialized,c2
is copy-initialized from a value initialized temporary. Any members ofc1
that have pod-type (or members of members, etc., etc.) may not be initialized if the user supplied default constructors (if any) do not explicitly initialize them. Forc2
, it depends on whether there is a user supplied copy constructor and whether that appropriately initializes those members, but the members of the temporary will all be initialized (zero-initialized if not otherwise explicitly initialized). As litb spotted,c3
is a trap. It's actually a function declaration.关于这部分的回答:
由于大多数答案都是 c++11 之前的,所以我添加了 c++11 对此的说明:
因此,无论是否优化,根据标准它们都是等效的。
请注意,这与其他答案提到的一致。 为了正确起见,只是引用标准的内容。
Answering with respect to this part:
Since most of the answers are pre-c++11 I am adding what c++11 has to say about this:
So optimization or not they are equivalent as per the standard.
Note that this is in accordance with what other answers have mentioned. Just quoting what the standard has to say for sake of correctness.
这是来自 Bjarne Stroustrup 的 C++ 编程语言:
带有 = 的初始化被视为复制初始化。 原则上,初始化器的副本(我们从中复制的对象)被放置到已初始化的对象中。 然而,这样的副本可以被优化掉(省略),并且如果初始值设定项是右值,则可以使用移动操作(基于移动语义)。 省略 = 会使初始化变得明确。 显式初始化称为“直接初始化”。
This is from C++ Programming Language by Bjarne Stroustrup:
An initialization with an = is considered a copy initialization. In principle, a copy of the initializer (the object we are copying from) is placed into the initialized object. However, such a copy may be optimized away (elided), and a move operation (based on move semantics) may be used if the initializer is an rvalue. Leaving out the = makes the initialization explicit. Explicit initialization is known as direct initialization.
很多情况都取决于对象的实现,因此很难给您具体的答案。
考虑这种情况
在这种情况下,假设有一个正确的赋值运算符 & 初始化接受单个整数参数的构造函数,我如何实现所述方法会影响每行的行为。 然而,通常的做法是其中一个在实现中调用另一个来消除重复的代码(尽管在如此简单的情况下没有真正的目的。)
编辑:正如其他响应中提到的,第一行将实际上调用了复制构造函数。 将与赋值运算符相关的注释视为与独立赋值相关的行为。
也就是说,编译器如何优化代码将产生其自身的影响。 如果我有调用“=”运算符的初始化构造函数 - 如果编译器没有进行优化,那么顶行将执行 2 次跳转,而不是底行中的 1 次。
现在,对于最常见的情况,您的编译器将通过这些情况进行优化并消除此类低效率。 因此,实际上,您描述的所有不同情况都会产生相同的结果。 如果您想准确了解正在执行的操作,可以查看编译器的目标代码或汇编输出。
A lot of these cases are subject to an object's implementation so it's hard to give you a concrete answer.
Consider the case
In this case assuming a proper assignment operator & initializing constructor which accept a single integer argument, how I implement said methods affects the behavior of each line. It is common practice however for one of those to call the other in the implementation as to eliminate duplicate code (although in a case as simple as this there would be no real purpose.)
Edit: As mentioned in other responses, the first line will in fact call the copy constructor. Consider the comments relating to the assignment operator as behavior pertaining to a stand alone assignment.
That said, how the compiler optimizes the code will then have it's own impact. If I have the initializing constructor calling the "=" operator - if the compiler makes no optimizations, the top line would then perform 2 jumps as opposed to one in the bottom line.
Now, for the most common situations, your compiler will optimize through these cases and eliminate this type of inefficiencies. So effectively all the different situations you describe will turn out the same. If you want to see exactly what is being done, you can look at the object code or an assembly output of your compiler.