假设我有这样一个函数:
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()); }
在每一组中,这些陈述是否相同?或者在某些初始化中是否存在额外的(可能是可优化的)副本?
我见过有人两种说法都说。请引用文本作为证明。也请添加其他案例。
赋值不同于初始化。
下面两行都执行初始化。一个构造函数调用完成:
A a1 = A_factory_func(); // calls copy constructor A a1(A_factory_func()); // calls copy constructor
但它不等于:
A a1; // calls default constructor a1 = A_factory_func(); // (assignment) calls operator =
我现在还没有一篇文章来证明这一点,但这很容易实验:
#include <iostream> using namespace std; class A { public: A() { cout << "default constructor" << endl; } A(const A& x) { cout << "copy constructor" << endl; } const A& operator = (const A& x) { cout << "operator =" << endl; return *this; } }; int main() { A a; // default constructor A b(a); // copy constructor A c = a; // copy constructor c = b; // operator = return 0; }
很多情况都取决于对象的实现,所以很难给你一个具体的答案。
考虑一下这个案例
A a = 5; A a(5);
在这种情况下,假设有一个合适的赋值运算符&初始化构造函数接受单个整数参数,我如何实现这些方法影响每一行的行为。然而,通常的做法是,其中一个在实现中调用另一个,以消除重复的代码(尽管在这样简单的情况下,没有真正的目的)。
编辑:正如在其他响应中提到的,第一行实际上将调用复制构造函数。将与赋值操作符相关的注释视为与独立赋值相关的行为。
也就是说,编译器如何优化代码会有它自己的影响。如果初始化构造函数调用“=”运算符——如果编译器不做任何优化,则顶部行将执行两次跳转,而底部行则执行一次跳转。
现在,对于最常见的情况,编译器将优化这些情况并消除这种低效率。所以实际上你所描述的所有不同情况都是一样的。如果您想确切地了解正在执行的操作,可以查看编译器的目标代码或汇编输出。
double b1 = 0.5;是构造函数的隐式调用。
double b1 = 0.5;
double b2(0.5);是显式调用。
double b2(0.5);
看看下面的代码,看看区别:
#include <iostream> class sss { public: explicit sss( int ) { std::cout << "int" << std::endl; }; sss( double ) { std::cout << "double" << std::endl; }; }; int main() { sss ddd( 7 ); // calls int constructor sss xxx = 7; // calls double constructor return 0; }
如果类没有显式构造函数,则显式调用和隐式调用是相同的。
第一个分组:它取决于A_factory_func返回什么。第一行是复制初始化的例子,第二行是A_factory_func0。如果A_factory_func返回一个A对象,那么它们是等价的,它们都调用A的复制构造函数,否则第一个版本从返回类型A_factory_func的可用转换操作符或适当的A构造函数中创建一个类型为A的右值,然后调用复制构造函数从这个临时对象构造a1。第二个版本试图找到一个合适的构造函数,该构造函数接受A_factory_func返回的任何值,或者接受返回值可以隐式转换为的值。
A_factory_func
A
a1
第二组:逻辑完全相同,只是内建类型没有任何奇异构造函数,因此它们实际上是相同的。
第三组:c1是默认初始化的,c2是从一个临时初始化的值复制初始化的。c1中任何具有pod-type的成员(或成员的成员,等等)都不能被初始化,如果用户提供的默认构造函数(如果有的话)没有显式初始化它们。对于c2,它取决于是否有用户提供的复制构造函数,以及该构造函数是否适当地初始化了这些成员,但临时对象的成员都将被初始化(如果没有显式初始化,则为零初始化)。正如litb所指出的,c3是一个陷阱。它实际上是一个函数声明。
c1
c2
c3
在c++ 17中,A_factory_func()的含义从创建临时对象(c++ <=14)转变为仅仅指定在c++ 17中这个表达式初始化到的任何对象的初始化(松散地说)。这些对象(称为“结果对象”)是由声明创建的变量(如a1),当初始化最终被丢弃时创建的人工对象,或者如果引用绑定需要一个对象(如A_factory_func();中)。在最后一种情况下,对象是人为创建的,称为“临时物化”,因为A_factory_func()没有一个变量或引用,否则需要一个对象存在)。
A_factory_func()
A_factory_func();
作为本例中的例子,在a1和a2的情况下,特殊规则说在这样的声明中,与a1相同类型的prvalue初始化器的结果对象是变量a1,因此A_factory_func()直接初始化对象a1。任何中间函数风格的强制转换都不会产生任何影响,因为A_factory_func(another-prvalue)只是将外部prvalue的结果对象“传递”为内部prvalue的结果对象。
a2
A_factory_func(another-prvalue)
A a1 = A_factory_func(); A a2(A_factory_func());
取决于A_factory_func()返回什么类型。我假设它返回A -那么它也做同样的事情-除了当复制构造函数是显式的,那么第一个将失败。读8.6/14
double b1 = 0.5; double b2(0.5);
因为它是内置类型(这里的意思是不是类类型),所以执行相同的操作。读8.6/14。
A c1; A c2 = A(); A c3(A());
这不是做同样的事情。如果A是非POD,则第一个默认初始化,并且不会对POD进行任何初始化(读取8.6/9)。第二个副本初始化:值初始化临时对象,然后将该值复制到c2(读取5.2.3/2和8.6/14)。这当然需要一个非显式的复制构造函数(读取8.6/14和12.3.1/3和c20)。第三种方法为函数c3创建一个函数声明,该函数返回A,并接受一个指向返回A的函数的函数指针(读为c21)。
深入研究初始化直接复制初始化
虽然它们看起来一模一样,而且应该做同样的事情,但在某些情况下,这两种形式有显著的不同。初始化有两种形式:直接初始化和复制初始化:
T t(x); T t = x;
我们可以把它们各自的行为归结为:
T
explicit
x
如你所见,复制初始化在某种程度上是直接初始化的一部分,涉及到可能的隐式转换:直接初始化有所有可用的构造函数可调用,而除了可以进行任何隐式转换,它需要匹配参数类型,复制初始化只能设置一个隐式转换序列。
我努力尝试了得到下面的代码,为每个这些形式输出不同的文本,没有使用“明显的”通过explicit构造函数。
#include <iostream> struct B; struct A { operator B(); }; struct B { B() { } B(A const&) { std::cout << "<direct> "; } }; A::operator B() { std::cout << "<copy> "; return B(); } int main() { A a; B b1(a); // 1) B b2 = a; // 2) } // output: <direct> <copy>
它是如何工作的,为什么输出这个结果?
首先,它对转换一无所知。它会尝试调用一个构造函数。在这种情况下,以下构造函数是可用的,并且是精确匹配:
B(A const&)
调用该构造函数不需要转换,更不用说用户定义的转换了(注意这里也没有发生const限定转换)。直接初始化会调用它。< / p >
如上所述,复制初始化将在a没有类型B或从它派生(这里显然是这种情况)时构造一个转换序列。因此,它将寻找进行转换的方法,并将找到以下候选方法
a
B
B(A const&) operator B(A&);
注意我是如何重写转换函数的:形参类型反映了this指针的类型,它在非const成员函数中是指向非const的。现在,我们用x作为参数调用这些候选对象。胜出的是转换函数:因为如果我们有两个候选函数都接受对同一类型的引用,那么更少的常量版本胜出(顺便说一下,这也是一种机制,它更倾向于非const成员函数调用非const对象)。
this
注意,如果将转换函数更改为const成员函数,则转换具有二义性(因为两者的形参类型都为A const&): Comeau编译器会正确拒绝它,但GCC会以非学院型模式接受它。不过,切换到-pedantic也会使它输出适当的歧义警告。< / p >
A const&
-pedantic
我希望这能让你更清楚这两种形式的区别!
值得注意的是:
[12.2/1] Temporaries of class type are created in various contexts: ... and in some initializations (8.5).
Temporaries of class type are created in various contexts: ... and in some initializations (8.5).
例如,用于复制初始化。
[12.8/15] When certain criteria are met, an implementation is allowed to omit the copy construction of a class object ...
When certain criteria are met, an implementation is allowed to omit the copy construction of a class object ...
换句话说,一个好的编译器会不在可以避免拷贝初始化时创建一个副本;相反,它将直接调用构造函数——即,就像直接初始化一样。
换句话说,复制初始化在大多数情况下就像直接初始化一样。这里已经编写了可理解的代码。由于直接初始化可能会导致任意(因此可能是未知的)转换,我更喜欢在可能的情况下总是使用复制初始化。(额外的好处是,它实际上看起来像初始化。)<
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.
很高兴我不是在写c++编译器。
就这部分回答:
A c2 = A();c3 (());
由于大多数答案都是c++11之前的,我补充了c++11必须说的:
一个简单类型说明符(7.1.6.2)或typename-specifier (14.6) 的表达式列表的值 给定表达式列表的指定类型。如果表达式列表是a 单个表达式时,类型转换表达式是等效的(in 定义性(如果在意义上定义)转换为相应的类型转换 表达式(5.4)。如果指定的类型是类类型,则类 型号应完整。如果表达式列表指定多于a 单个值时,类型应是具有适当声明的类 构造函数(8.5,12.1),表达式T(x1, x2,…)为 等效于声明T T (x1, x2,…); 发明临时变量t,结果是t的值为 prvalue。< / p >
所以不管是否优化,根据标准它们是等价的。 请注意,这与前面提到的其他答案是一致的。只是为了正确起见,引用了标准的内容。< / p >
在初始化对象时,可以看到explicit和implicit构造函数类型的区别:
implicit
类:
class A { A(int) { } // converting constructor A(int, int) { } // converting constructor (C++11) }; class B { explicit B(int) { } explicit B(int, int) { } };
__abc1 __abc0 __abc2
int main() { A a1 = 1; // OK: copy-initialization selects A::A(int) A a2(2); // OK: direct-initialization selects A::A(int) A a3 {4, 5}; // OK: direct-list-initialization selects A::A(int, int) A a4 = {4, 5}; // OK: copy-list-initialization selects A::A(int, int) A a5 = (A)1; // OK: explicit cast performs static_cast // B b1 = 1; // error: copy-initialization does not consider B::B(int) B b2(2); // OK: direct-initialization selects B::B(int) B b3 {4, 5}; // OK: direct-list-initialization selects B::B(int, int) // B b4 = {4, 5}; // error: copy-list-initialization does not consider B::B(int,int) B b5 = (B)1; // OK: explicit cast performs static_cast }
默认情况下,构造函数是implicit,所以你有两种方法来初始化它:
A a1 = 1; // this is copy initialization A a2(2); // this is direct initialization
通过将结构体定义为explicit,你只有一种直接方式:
B b2(2); // this is direct initialization B b5 = (B)1; // not problem if you either use of assign to initialize and cast it as static_cast
这是来自Bjarne Stroustrup的c++编程语言:
带有=的初始化被认为是复制初始化。原则上,初始化器(我们要从中复制的对象)的副本被放置到初始化的对象中。但是,这样的副本可以被优化掉(省略),如果初始化式是右值,则可以使用move操作(基于move语义)。省略=可以显式地初始化。显式初始化称为直接初始化。