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

rlb*_*ond 230 c++ initialization

假设我有这个功能:

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());
}
Run Code Online (Sandbox Code Playgroud)

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

我见过有人说过这两件事.请引用文字作为证据.还请添加其他案例.

Joh*_*itb 242

C++ 17更新

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

如在我们的情况下的例子,在的情况下,a1a2特别规则说,在这样的声明,相同的类型的一个prvalue初始化的结果对象a1是可变的a1,因此A_factory_func()直接初始化对象a1.任何中间函数式转换都不会产生任何影响,因为A_factory_func(another-prvalue)只是"传递"外部prvalue的结果对象也是内部prvalue的结果对象.


A a1 = A_factory_func();
A a2(A_factory_func());
Run Code Online (Sandbox Code Playgroud)

取决于什么类型A_factory_func()返回.我假设它返回一个A- 然后它正在做同样的事情 - 除了复制构造函数是显式的,然后第一个将失败.阅读8.6/14

double b1 = 0.5;
double b2(0.5);
Run Code Online (Sandbox Code Playgroud)

这样做是因为它是内置类型(这意味着这里不是类类型).阅读8.6/14.

A c1;
A c2 = A();
A c3(A());
Run Code Online (Sandbox Code Playgroud)

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


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

虽然它们看起来相同并且应该做同样的事情,但在某些情况下这两种形式却截然不同.两种形式的初始化是直接和复制初始化:

T t(x);
T t = x;
Run Code Online (Sandbox Code Playgroud)

我们可以将行为归因于每个行为:

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

如您所见,复制初始化在某种程度上是关于可能的隐式转换的直接初始化的一部分:虽然直接初始化具有可用于调用的所有构造函数,并且此外可以进行任何隐式转换,它需要匹配参数类型,复制初始化可以设置一个隐式转换序列.

我努力尝试并获得以下代码,为每个表单输出不同的文本,而不使用"明显的"通过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>
Run Code Online (Sandbox Code Playgroud)

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

  1. 直接初始化

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

    B(A const&)
    
    Run Code Online (Sandbox Code Playgroud)

    调用该构造函数不需要转换,更不用说用户定义的转换了(请注意,此处也不会发生const限定转换).所以直接初始化会调用它.

  2. 复制初始化

    如上所述,复制初始化将在a未输入B或从中导出时构建转换序列(这里显然是这种情况).因此,它将寻找进行转换的方法,并将找到以下候选人

    B(A const&)
    operator B(A&);
    
    Run Code Online (Sandbox Code Playgroud)

    注意我是如何重写转换函数的:参数类型反映了this指针的类型,在非const成员函数中它是非const的.现在,我们将这些候选人x称为争论.获胜者是转换函数:因为如果我们有两个候选函数都接受对相同类型的引用,则较少的const版本获胜(顺便说一下,这也是非const成员函数调用非机制的机制) -const对象).

    请注意,如果我们将转换函数更改为const成员函数,则转换是不明确的(因为它们都具有A const&当时的参数类型):Comeau编译器正确拒绝它,但GCC在非迂腐模式下接受它.但是切换到-pedantic它也会输出正确的模糊警告.

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

  • @AzP很多关于SO的人经常想要引用C++规范,这就是我在这里所做的,以回应rlbond的请求"请引用文本作为证据.".我不想引用这个规范,因为这会增加我的答案,并且需要做更多工作来保持最新(冗余). (9认同)
  • 呸,对不起朋友,但我不得不删除我的评论并再次发布,因为新的格式化引擎:这是因为在函数参数中,`R()== R(*)()`和`T [] == T*`.也就是说,函数类型是函数指针类型,数组类型是指向元素的类型.这很糟糕.它可以通过`A c3((A()));(表达式周围的parens)来解决. (4认同)
  • 请问"'Read 8.5/14'"是什么意思?那指的是什么?一本书?一章?一个网站? (4认同)

Meh*_*ari 46

分配初始化不同.

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

A a1 = A_factory_func();  // calls copy constructor
A a1(A_factory_func());   // calls copy constructor
Run Code Online (Sandbox Code Playgroud)

但它并不等同于:

A a1;                     // calls default constructor
a1 = A_factory_func();    // (assignment) calls operator =
Run Code Online (Sandbox Code Playgroud)

我目前没有文字来证明这一点,但实验起来很容易:

#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;
}
Run Code Online (Sandbox Code Playgroud)

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

Kir*_*sky 17

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;
}
Run Code Online (Sandbox Code Playgroud)

如果你的类没有显式的构造,那么显式和隐式调用是相同的.

  • +1.很好的答案.很好还要注意显式版本.顺便说一句,重要的是要注意你不能同时拥有单个构造函数的**版本*.因此,它只能在明确的情况下编译.如果他们都编译,他们必须表现相似. (5认同)
  • 这一定是公认的答案!简短而清晰的例子。 (3认同)

Joh*_* H. 5

值得注意的是

[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++编译器.


Bat*_*ted 5

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

课程:

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

class B
{
    explicit B(int) { }
    explicit B(int, int) { }
};
Run Code Online (Sandbox Code Playgroud)

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
}
Run Code Online (Sandbox Code Playgroud)

默认情况下,构造函数是implicit这样你有两种方法来初始化它:

A a1 = 1;        // this is copy initialization
A a2(2);         // this is direct initialization
Run Code Online (Sandbox Code Playgroud)

通过定义一个结构,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
Run Code Online (Sandbox Code Playgroud)


归档时间:

查看次数:

48699 次

最近记录:

6 年,2 月 前