代码之家  ›  专栏  ›  技术社区  ›  rlbond

拷贝初始化和直接初始化有区别吗?

  •  214
  • rlbond  · 技术社区  · 16 年前

    假设我有这个功能:

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

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

    我见过人们说这两件事。拜托 引用 文本作为证明。另外请加上其他箱子。

    8 回复  |  直到 7 年前
        1
  •  230
  •   TechnicallyTrue Rohit Jain    7 年前

    C++ 17更新

    在C++ 17中,意思是 A_factory_func() 从创建临时对象(c++ & lt;=14)更改为只指定在C++ 17中对该表达式初始化为(松散地说)的任何对象的初始化。这些对象(称为“结果对象”)是由声明创建的变量(如 a1 ,当初始化被丢弃或引用绑定需要对象时创建的人工对象(如 A_factory_func(); . 在最后一种情况下,对象是人工创建的,称为“临时物化”,因为 工厂 没有变量或引用,否则将需要对象存在)。

    例如,在我们的案例中, A1 a2 特殊规则规定,在这种声明中,prvalue初始值设定项的结果对象与 A1 是可变的 A1 因此 工厂 直接初始化对象 A1 . 任何中间功能样式转换都不会有任何效果,因为 A_factory_func(another-prvalue) 只是“通过”外部prvalue的结果对象也是内部prvalue的结果对象。


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

    取决于什么类型 工厂 返回。我想它会返回一个 A -然后它也在做同样的事情——除了当复制构造函数是显式的,那么第一个就会失败。读 8.6/14

    double b1 = 0.5;
    double b2(0.5);
    

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

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

    这样做是不一样的。如果 是非pod,不为pod进行任何初始化(读取 8.6/9 )第二个副本初始化:值初始化临时值,然后将该值复制到 c2 (阅读) 5.2.3/2 86/14 )当然,这需要一个非显式的复制构造函数(读 86/14 12.3.1/3 13.3.1.3/1 )第三个为函数创建函数声明 c3 返回一个 它将函数指针指向返回 (阅读) 8.2 )


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

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

    T t(x);
    T t = x;
    

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

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

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

    我努力尝试过 got the following code to output different text for each of those forms 不使用“明显”的 构造函数。

    #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&)
      

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

    2. 复制初始化

      如上所述,复制初始化将在 a 没有类型 B 或者从中衍生出来(这里很明显就是这样)。因此,它将寻找进行转换的方法,并将找到以下候选人

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

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

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

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

        2
  •  44
  •   Mehrdad Afshari    16 年前

    转让 不同于 初始化 .

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

    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;
    }
    
        3
  •  15
  •   Kirill V. Lyadvinsky    16 年前

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

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

        4
  •  5
  •   John H.    14 年前

    值得注意的:

    〔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++编译器。

        5
  •  4
  •   CB Bailey    16 年前

    第一组:取决于 A_factory_func 返回。第一行是 复制初始化 直接初始化 . 如果 阿氏因子 返回一个 A 对象,然后它们是等效的,它们都调用 否则,第一个版本将创建类型为的右值 从返回类型的可用转换运算符 阿氏因子 或适当的 构造函数,然后调用复制构造函数来构造 a1 从这个临时的。第二个版本试图找到一个合适的构造函数 阿氏因子 返回,或者获取返回值可以隐式转换为的内容。

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

    第三分组: c1 是默认初始化, c2 是从临时初始化的值初始化的复制。任何成员 C1 如果用户提供的默认构造函数(如果有)没有显式初始化pod类型(或成员成员成员等)的,则不能初始化它们。为了 C2 ,这取决于是否存在用户提供的复制构造函数,以及是否适当地初始化了这些成员,但临时成员将全部初始化(如果没有以其他方式显式初始化,则初始化为零)。当李特发现的时候, c3 是陷阱。它实际上是一个函数声明。

        6
  •  4
  •   bashrc    10 年前

    关于本部分的回答:

    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(x1,x2,…); 对于一些 发明了临时变量t,其结果是t的值为 一个PR值。

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

        7
  •  1
  •   dborba    16 年前

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

    考虑一下这个案子

    A a = 5;
    A a(5);
    

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

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

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

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

        8
  •  1
  •   BattleTested_закалённый в бою Fritz R.    7 年前

    你可以看到它的区别 explicit implicit 初始化对象时的构造函数类型:

    Classes:

    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
    }
    

    默认情况下,构造函数是 隐性的 所以您有两种方法来初始化它:

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

    通过将结构定义为 明确的 只是你有一个直接的方法:

    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