关于c ++:复制初始化和直接初始化之间有区别吗?

Is there a difference between copy initialization and direct initialization?

我想这个功能:

1
2
3
4
5
6
7
8
9
10
11
12
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());
}

在每个分组的报表,这些是相同的吗?或有额外的(可能是优化的复制和初始化)?

我有两个湖泊的人说的事情。请引用文本作为证据。所以,请添加其他案件。


C++ 17更新

在C++ 17中,EDCOX1(0)的含义从创建临时对象(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的结果对象。好的。

1
2
A a1 = A_factory_func();
A a2(A_factory_func());

取决于A_factory_func()返回的类型。我假设它返回一个A,那么它也会这样做,除非当复制构造函数是显式的,那么第一个构造函数就会失败。读数为86/14好的。

1
2
double b1 = 0.5;
double b2(0.5);

这样做是因为它是一个内置类型(这意味着这里不是类类型)。读数为86/14。好的。

1
2
3
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和13.3.1.3/1)。第三个函数为函数c3创建函数声明,该函数返回A,并接受函数指针指向返回A的函数(见8.2)。好的。

深入了解初始化直接和复制初始化好的。

虽然它们看起来是相同的,应该是相同的,但在某些情况下,这两种形式是显著不同的。初始化的两种形式是直接初始化和复制初始化:好的。

1
2
T t(x);
T t = x;

我们可以将行为归因于它们中的每一个:好的。

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

如您所见,复制初始化在某种程度上是关于可能的隐式转换的直接初始化的一部分:虽然直接初始化有所有可调用的构造函数,而且可以执行任何隐式转换,以匹配参数类型,但复制初始化只能设置一个隐式转换序列。好的。

我尝试了一下,得到了以下代码来为每个表单输出不同的文本,而不使用通过explicit构造函数的"明显"代码。好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#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&)

    调用该构造函数不需要转换,更不用说用户定义的转换(注意这里也没有常量限定转换)。所以直接初始化会调用它。好的。

  • 复制初始化好的。

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

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

    请注意,我如何重写转换函数:参数类型反映了this指针的类型,在非常量成员函数中,该指针指向非常量。现在,我们称这些候选人为x作为论据。胜利者是转换函数:因为如果我们有两个候选函数都接受对同一类型的引用,那么较少的const版本将获胜(顺便说一下,这也是偏好非const成员函数的机制调用非const对象)。好的。

    注意,如果我们将转换函数更改为常量成员函数,那么转换是不明确的(因为这两个函数的参数类型都是A const&):comeau编译器正确地拒绝了它,但gcc以非学究模式接受它。不过,切换到-pedantic也会使它输出适当的模糊警告。好的。

  • 我希望这有助于更清楚地说明这两种形式的区别!好的。好啊。


    分配不同于初始化。

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

    1
    2
    A a1 = A_factory_func();  // calls copy constructor
    A a1(A_factory_func());   // calls copy constructor

    但这并不等同于:

    1
    2
    A a1;                     // calls default constructor
    a1 = A_factory_func();    // (assignment) calls operator =

    我现在没有文本来证明这一点,但是很容易实验:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    #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;
    }


    double b1 = 0.5;是构造函数的隐式调用。

    double b2(0.5);是显式调用。

    请查看以下代码以查看区别:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #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;
    }

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


    值得注意的:

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

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

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

    技术类别:[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++编译器。


    关于本部分的回答:

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

    因为大多数答案都是前C ++ 11,所以我添加了C++ 11对此的说法:

    A simple-type-specifier (7.1.6.2) or typename-specifier (14.6)
    followed by a parenthesized expression-list constructs a value of the
    specified type given the expression list. If the expression list is a
    single expression, the type conversion expression is equivalent (in
    definedness, and if defined in meaning) to the corresponding cast
    expression (5.4). If the type specified is a class type, the class
    type shall be complete. If the expression list specifies more than a
    single value, the type shall be a class with a suitably declared
    constructor (8.5, 12.1), and the expression T(x1, x2, ...) is
    equivalent in effect to the declaration T t(x1, x2, ...); for some
    invented temporary variable t, with the result being the value of t as
    a prvalue.

    因此,无论优化与否,它们都是符合标准的等价物。请注意,这与其他答案所提到的是一致的。为了正确起见,只需引用标准所说的话。


    第一组:取决于A_factory_func返回的是什么。第一行是复制初始化的示例,第二行是直接初始化。如果A_factory_func返回一个A对象,那么它们是等价的,它们都调用A的复制构造函数,否则,第一个版本从可用的转换运算符为A_factory_func或适当的A构造函数的返回类型创建一个A类型的右值,然后调用复制构造函数。Tor从这个临时工程中建造a1。第二个版本试图找到一个合适的构造函数,它接受A_factory_func返回的任何内容,或者接受返回值可以隐式转换为的内容。

    第二组:完全相同的逻辑,只是内置类型没有任何外来的构造函数,所以实际上它们是相同的。

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


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

    Classes:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class A
    {
        A(int) { }      // converting constructor
        A(int, int) { } // converting constructor (C++11)
    };

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

    main函数中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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,因此有两种初始化方法:

    1
    2
    A a1 = 1;        // this is copy initialization
    A a2(2);         // this is direct initialization

    通过将一个结构定义为explicit,您可以直接使用一种方法:

    1
    2
    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

    很多这样的情况都取决于对象的实现,因此很难给出具体的答案。

    考虑一下这个案子

    1
    2
    A a = 5;
    A a(5);

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

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

    这就是说,编译器如何优化代码将有它自己的影响。如果我有一个初始化构造函数调用"="运算符-如果编译器没有进行优化,那么顶行将执行2次跳转,而不是底行中的一次跳转。

    现在,对于最常见的情况,编译器将通过这些情况进行优化,并消除这种效率低下的情况。所以实际上,你描述的所有不同的情况都是一样的。如果您想确切地看到正在执行的操作,可以查看对象代码或编译器的程序集输出。