复制初始化和直接初始化之间有区别吗?

假设我有这样一个函数:

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());
}

在每一组中,这些陈述是否相同?或者在某些初始化中是否存在额外的(可能是可优化的)副本?

我见过有人两种说法都说。请引用文本作为证明。也请添加其他案例。

70607 次浏览

赋值不同于初始化

下面两行都执行初始化。一个构造函数调用完成:

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 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返回的任何值,或者接受返回值可以隐式转换为的值。

第二组:逻辑完全相同,只是内建类型没有任何奇异构造函数,因此它们实际上是相同的。

第三组:c1是默认初始化的,c2是从一个临时初始化的值复制初始化的。c1中任何具有pod-type的成员(或成员的成员,等等)都不能被初始化,如果用户提供的默认构造函数(如果有的话)没有显式初始化它们。对于c2,它取决于是否有用户提供的复制构造函数,以及该构造函数是否适当地初始化了这些成员,但临时对象的成员都将被初始化(如果没有显式初始化,则为零初始化)。正如litb所指出的,c3是一个陷阱。它实际上是一个函数声明。

c++ 17更新

在c++ 17中,A_factory_func()的含义从创建临时对象(c++ <=14)转变为仅仅指定在c++ 17中这个表达式初始化到的任何对象的初始化(松散地说)。这些对象(称为“结果对象”)是由声明创建的变量(如a1),当初始化最终被丢弃时创建的人工对象,或者如果引用绑定需要一个对象(如A_factory_func();中)。在最后一种情况下,对象是人为创建的,称为“临时物化”,因为A_factory_func()没有一个变量或引用,否则需要一个对象存在)。

作为本例中的例子,在a1a2的情况下,特殊规则说在这样的声明中,与a1相同类型的prvalue初始化器的结果对象是变量a1,因此A_factory_func()直接初始化对象a1。任何中间函数风格的强制转换都不会产生任何影响,因为A_factory_func(another-prvalue)只是将外部prvalue的结果对象“传递”为内部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/28.6/14)。这当然需要一个非显式的复制构造函数(读取8.6/1412.3.1/3c20)。第三种方法为函数c3创建一个函数声明,该函数返回A,并接受一个指向返回A的函数的函数指针(读为c21)。


深入研究初始化直接复制初始化

虽然它们看起来一模一样,而且应该做同样的事情,但在某些情况下,这两种形式有显著的不同。初始化有两种形式:直接初始化和复制初始化:

T t(x);
T t = x;

我们可以把它们各自的行为归结为:

  • 直接初始化的行为类似于重载函数的函数调用:在这种情况下,函数是T的构造函数(包括explicit的构造函数),参数是x。重载解析将找到最佳匹配的构造函数,并在需要时执行所需的任何隐式转换。
  • 复制初始化构造一个隐式转换序列:它尝试将x转换为类型为T的对象。(然后它可以将该对象复制到要初始化的对象中,因此也需要一个复制构造函数-但这在下面并不重要)

如你所见,复制初始化在某种程度上是直接初始化的一部分,涉及到可能的隐式转换:直接初始化有所有可用的构造函数可调用,而除了可以进行任何隐式转换,它需要匹配参数类型,复制初始化只能设置一个隐式转换序列。

我努力尝试了得到下面的代码,为每个这些形式输出不同的文本,没有使用“明显的”通过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>

它是如何工作的,为什么输出这个结果?

  1. < p > 直接初始化

    首先,它对转换一无所知。它会尝试调用一个构造函数。在这种情况下,以下构造函数是可用的,并且是精确匹配:

    B(A const&)
    

    调用该构造函数不需要转换,更不用说用户定义的转换了(注意这里也没有发生const限定转换)。直接初始化会调用它。< / p >

  2. < p > 复制初始化

    如上所述,复制初始化将在a没有类型B或从它派生(这里显然是这种情况)时构造一个转换序列。因此,它将寻找进行转换的方法,并将找到以下候选方法

    B(A const&)
    operator B(A&);
    

    注意我是如何重写转换函数的:形参类型反映了this指针的类型,它在非const成员函数中是指向非const的。现在,我们用x作为参数调用这些候选对象。胜出的是转换函数:因为如果我们有两个候选函数都接受对同一类型的引用,那么更少的常量版本胜出(顺便说一下,这也是一种机制,它更倾向于非const成员函数调用非const对象)。

    注意,如果将转换函数更改为const成员函数,则转换具有二义性(因为两者的形参类型都为A const&): Comeau编译器会正确拒绝它,但GCC会以非学院型模式接受它。不过,切换到-pedantic也会使它输出适当的歧义警告。< / p >

我希望这能让你更清楚这两种形式的区别!

值得注意的是:

[12.2/1] 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 ...

换句话说,一个好的编译器会在可以避免拷贝初始化时创建一个副本;相反,它将直接调用构造函数——即,就像直接初始化一样。

换句话说,复制初始化在大多数情况下就像直接初始化一样。这里已经编写了可理解的代码。由于直接初始化可能会导致任意(因此可能是未知的)转换,我更喜欢在可能的情况下总是使用复制初始化。(额外的好处是,它实际上看起来像初始化。)<

< p >技术血淋淋的景象: [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.

很高兴我不是在写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 >

在初始化对象时,可以看到explicitimplicit构造函数类型的区别:

类:

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语义)。省略=可以显式地初始化。显式初始化称为直接初始化