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


244

假设我有这个功能:

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

在每个分组中,这些语句是否相同?还是在某些初始化中有额外的(可能是可优化的)副本?

我已经看到人们都说两件事。请引用文字作为证据。还请添加其他情况。


1
@JohannesSchaub-讨论了第四种情况A c1; A c2 = c1; A c3(c1);
Dan Nissenbaum 2015年

1
只是2018年的注释:规则在C ++ 17中已更改,请参见例如此处。如果我的理解是正确的,则在C ++ 17中,两个语句实际上是相同的(即使复制ctor是显式的)。此外,如果init表达式的类型不是A,则复制初始化将不需要复制/移动构造函数。这就是为什么std::atomic<int> a = 1;在C ++ 17中没问题,但以前没问题的原因。
Daniel Langr

Answers:


246

C ++ 17更新

在C ++ 17中,含义A_factory_func()从创建一个临时对象(C ++ <= 14)变为仅指定对该表达式在C ++ 17中初始化为(宽松地说)的任何对象的初始化。这些对象(称为“结果对象”)是由声明(如a1)创建的变量,是在初始化结束时被丢弃的人造对象,或者如果引用绑定需要对象(如中的in)A_factory_func();。对象是人为创建的,称为“临时实现”,因为A_factory_func()它没有变量或引用,否则将需要对象存在。

作为示例,在a1a2特殊规则的情况下,在这样的声明中,同类型的prvalue初始化器的结果对象a1是variable a1,因此A_factory_func()直接初始化object 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());

这是不一样的。第一个默认初始化if A是非POD,并且不对POD进行任何初始化(请参阅8.6 / 9)。第二个副本初始化:值-初始化一个临时值,然后将该值复制到c2(读取5.2.3 / 28.6 / 14)。当然,这将需要非显式的副本构造函数(请参阅8.6 / 1412.3.1 / 313.3.1.3/1)。第三个函数为c3返回an 的函数创建函数声明A,并使用指向返回a的函数的函数指针A(读8.2)。


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

尽管它们看起来相同并且应该做相同的事情,但是在某些情况下,这两种形式却有很大不同。初始化的两种形式是直接初始化和复制初始化:

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. 直接初始化

    首先,它对转换一无所知。它将仅尝试调用构造函数。在这种情况下,以下构造函数可用,并且完全匹配

    B(A const&)

    调用该构造函数不需要转换,更不用说用户定义的转换了(请注意,这里也没有进行const资格转换)。因此,直接初始化将调用它。

  2. 复制初始化

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

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

    请注意我如何重写转换函数:参数类型反映了this指针的类型,在非const成员函数中,该指针指向非const。现在,我们将这些候选对象x称为参数。赢家是转换函数:因为如果我们有两个候选函数都接受对相同类型的引用,则较少const版本会获胜(顺便说一句,这也是首选使用非const成员函数的机制将non称为-const对象)。

    请注意,如果将转换函数更改为const成员函数,则转换将是模棱两可的(因为两者的参数类型均为A const&then):Comeau编译器会正确拒绝它,但GCC在非non脚模式下接受它。切换到-pedantic它也会输出适当的歧义警告。

我希望这有助于使这两种形式之间的区别更加清楚!


哇。我什至没有意识到函数声明。我几乎必须接受您的回答,因为它是唯一知道这一点的人。函数声明有这种工作方式吗?如果在函数内部对c3进行不同的处理会更好。
rlbond

4
Bah,对不起大家,但是由于新的格式化引擎,我不得不删除我的评论并再次发布它:这是因为在函数参数中,R() == R(*)()T[] == T*。即,函数类型是函数指针类型,而数组类型是指针到元素的类型。糟透了 可以通过解决A c3((A()));(表达式周围的内容)。
Johannes Schaub-litb

4
请问“读8.5 / 14”是什么意思?那指的是什么?一本书?一章?一个网站?
AzP 2011年

9
@AzP SO上的许多人经常希望引用C ++规范,这就是我在这里所做的,以响应rlbond的请求“请引用文字作为证明”。我不想引用该规范,因为这会使我的回答肿,并且需要做很多工作来保持最新(冗余)。
Johannes Schaub-litb 2011年

1
@luca我建议为此提出一个新问题,以便其他人也可以从人们也给予的答案中受益
Johannes Schaub-litb

49

分配不同于初始化

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

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

2
良好的参考:Bjarne Stroustrup撰写的“ C ++编程语言,特别版”,第10.4.4.1节(第245页)。描述了副本初始化和副本分配以及它们之间根本不同的原因(尽管它们都使用=运算符作为语法)。
纳夫2009年

轻微,但我真的不喜欢人们说“ A a(x)”和“ A a = x”相等。严格来说不是。在很多情况下,它们会做完全相同的事情,但是可以创建示例,其中根据参数实际调用不同的构造函数。
理查德·科登

我不是在说“句法等效”。在语义上,两种初始化方式都相同。
Mehrdad Afshari

@MehrdadAfshari在Johannes的答案代码中,您将根据使用的两种方式中的哪一种获得不同的输出。
Brian Gordon

1
@BrianGordon是的,你是对的。它们不相等。很久以前,我已经在编辑中谈到了理查德的评论。
Mehrdad Afshari'7

22

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

如果您的类没有显式构造函数,则显式和隐式调用是相同的。


5
+1。好答案。还要注意显式版本。顺便说一句,重要的是要注意您不能同时拥有单个构造函数重载的两个版本。因此,它将无法在显式情况下进行编译。如果它们都编译,则它们的行为必须类似。
Mehrdad Afshari

4

第一个分组:取决于A_factory_func返回的内容。第一行是复制初始化的示例,第二行是直接初始化。如果A_factory_func返回一个A对象,然后它们是等价的,它们都呼吁拷贝构造函数A,否则第一个版本创建类型的右值A从一个可用的转换操作符的返回类型A_factory_func或适当的A构造函数,然后调用拷贝构造函数来构造a1从该临时。第二个版本试图找到一个合适的构造函数,该构造函数需要A_factory_func返回值,或者采用可以将返回值隐式转换为的值。

第二种分组:除了内置类型没有任何奇异的构造函数之外,它们的逻辑完全相同,因此它们在实践中是相同的。

第三组:c1默认初始化,c2从值初始化的临时副本复制初始化。的任何成员c1具有荚型(或成员的成员等,等),如果用户提供的默认构造(如果有的话)不明确地初始化它们可能不被初始化。对于c2,它取决于是否有用户提供的副本构造函数以及是否适当地初始化了这些成员,但是临时成员将全部被初始化(如果没有另外进行显式初始化,则为零初始化)。正如一针见血,c3是一个陷阱。它实际上是一个函数声明。


4

注意:

[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 ...

换句话说,一个好的编译器在可以避免的情况下不会创建用于副本初始化的副本。相反,它将直接调用构造函数-即,就像直接初始化一样。

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

技术上的顾虑:[来自上方的12.2 / 1,续] 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 ++编译器。


4

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

课程:

class A
{
    A(int) { }      // converting constructor
    A(int, int) { } // converting constructor (C++11)
};

class B
{
    explicit B(int) { }
    explicit B(int, int) { }
};

并在 main 函数中:

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
}

默认情况下,构造函数为as,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

3

关于这部分的回答:

a c2 = A(); a c3(A());

由于大多数答案是c ++ 11之前的版本,因此我要添加c ++ 11关于此的内容:

简单类型说明符(7.1.6.2)或类型名称说明符(14.6)后跟带括号的表达式列表,可在给定表达式列表的情况下构造指定类型的值。如果表达式列表是单个表达式,则类型转换表达式(在定义上以及含义上)等效于相应的转换表达式(5.4)。如果指定的类型是类类型,则该类类型应是完整的。如果表达式列表指定了多个值,则该类型应为带有适当声明的构造函数的类(8.5、12.1),并且表达式T(x1,x2,...)等效于声明T t (x1,x2,...); 对于某些发明的临时变量t,结果是t的值作为prvalue。

因此,优化与否等同于标准。请注意,这与所提到的其他答案一致。只是为了正确起见引用标准要说的话。


您的示例的“表达式列表均未指定多个值”。这有什么关系?
underscore_d

0

这些情况中的许多情况都取决于对象的实现,因此很难为您提供具体的答案。

考虑一下情况

A a = 5;
A a(5);

在这种情况下,假设接受单个整数参数的正确赋值运算符和初始化构造函数,则我实现上述方法的方式会影响每一行的行为。但是,通常的做法是,其中的一个在实现中调用另一个,以消除重复的代码(尽管在这种情况下如此简单,没有实际目的。)

编辑:如其他答复中所述,第一行实际上将调用复制构造函数。将与赋值运算符有关的注释视为与独立赋值有关的行为。

也就是说,编译器如何优化代码将产生自己的影响。如果我有初始化构造函数调用“ =”运算符-如果编译器未进行任何优化,则顶行将执行2次跳转,而不是底行执行一次。

现在,对于最常见的情况,您的编译器将通过这些情况进行优化,并消除这种类型的效率低下的情况。因此,实际上,您描述的所有不同情况都会变得相同。如果要确切了解正在执行的操作,则可以查看目标代码或编译器的程序集输出。


这不是优化。在这两种情况下,编译器都必须调用构造函数。结果,如果您只有operator =(const int)而没有,则它们都不会编译A(const int)。有关更多详细信息,请参见@ jia3ep的答案。
Mehrdad Afshari,2009年

我相信您实际上是正确的。但是,通过使用默认的复制构造函数,它将可以很好地进行编译。
dborba

另外,正如我所提到的,通常的做法是让复制构造函数调用赋值运算符,此时编译器优化确实起作用。
dborba

0

这是来自Bjarne Stroustrup的C ++编程语言:

用=进行的初始化被视为副本初始化。原则上,将初始化程序的副本(我们要从中复制的对象)放入初始化的对象中。但是,如果初始化程序是右值,则可以优化(删除)这样的副本,并且可以使用移动操作(基于移动语义)。省略=使初始化显式。显式初始化称为直接初始化

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.