【问题标题】:Is there a difference between copy initialization and direct initialization?复制初始化和直接初始化有区别吗?
【发布时间】:2010-11-06 06:54:01
【问题描述】:

假设我有这个功能:

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

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

我见过人们说这两种话。请引用文字作为证据。也请添加其他案例。

【问题讨论】:

  • 还有@JohannesSchaub 讨论的第四个案例——A c1; A c2 = c1; A c3(c1);
  • 只是 2018 年的注释: C++17 中的规则已更改,例如,请参阅 here。如果我的理解是正确的,那么在 C++17 中,两个语句实际上是相同的(即使复制 ctor 是显式的)。此外,如果 init 表达式的类型不是A,则复制初始化不需要存在复制/移动构造函数。这就是为什么std::atomic<int> a = 1; 在 C++17 中是可以的,但在之前不行。

标签: c++ initialization


【解决方案1】:

C++17 更新

在 C++17 中,A_factory_func() 的含义从创建临时对象 (C++a1),在初始化最终被丢弃时创建的人造对象,或者引用绑定需要一个对象(如A_factory_func(); . 在最后一种情况下,对象是人为创建的,称为“临时物化”,因为A_factory_func() 没有变量或引用,否则需要对象存在)。

作为我们案例中的示例,在 a1a2 的情况下,特殊规则说在此类声明中,与 a1 相同类型的纯右值初始化器的结果对象是变量 a1,并且因此A_factory_func() 直接初始化对象a1。任何中间函数式强制转换都不会产生任何效果,因为A_factory_func(another-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/313.3.1.3/1 )。第三个为函数c3 创建一个函数声明,该函数返回一个A,并接受一个函数指针,该函数指针指向一个返回A 的函数(阅读8.2)。


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

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

T t(x);
T t = x;

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

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

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

我努力尝试got the following code to output different text for each of those forms,没有通过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 指针的类型,它在非常量成员函数中是非常量的。现在,我们用x 作为参数来称呼这些候选人。获胜者是转换函数:因为如果我们有两个候选函数都接受对同一类型的引用,那么 less const 版本获胜(顺便说一下,这也是首选非-const 成员函数调用非常量对象)。

    请注意,如果我们将转换函数更改为 const 成员函数,则转换是不明确的(因为两者都有一个参数类型 A const&amp; 然后):Comeau 编译器正确拒绝它,但 GCC 在非-迂腐模式。不过,切换到-pedantic 也会使其输出正确的歧义警告。

我希望这有助于更清楚地了解这两种形式的不同之处!

【讨论】:

  • 哇。我什至没有意识到函数声明。我几乎不得不接受你的回答,因为我是唯一知道这一点的人。函数声明以这种方式工作是否有原因?如果 c3 在函数内部被区别对待会更好。
  • 呸,对不起,我不得不删除我的评论并再次发布,因为新的格式化引擎:这是因为在函数参数中,R() == R(*)()T[] == T*。也就是说,函数类型是函数指针类型,数组类型是指向元素的类型。这很糟糕。可以通过A c3((A()));(表达式周围的括号)来解决它。
  • 请问“'Read 8.5/14'”是什么意思?那指的是什么?一本书?一章?网站?
  • @AzP SO 上的许多人经常希望引用 C++ 规范,这就是我在这里所做的,以响应 rlbond 的请求“请引用文本作为证据。”。我不想引用规范,因为这会使我的答案变得臃肿,并且需要做更多的工作来保持最新(冗余)。
  • @luca 我建议为此提出一个新问题,以便其他人也可以从人们给出的答案中受益
【解决方案2】:

赋值不同于初始化

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

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

【讨论】:

  • 很好的参考资料:Bjarne Stroustrup 的“C++ 编程语言,特别版”,第 10.4.4.1 节(第 245 页)。描述复制初始化和复制赋值,以及它们根本不同的原因(尽管它们都使用 = 运算符作为语法)。
  • 次要,但我真的不喜欢人们说“A a( x )”和“A a = x”是相等的。严格来说,他们不是。在很多情况下,它们会做完全相同的事情,但可以创建示例,根据参数实际调用不同的构造函数。
  • 我不是在谈论“句法对等”。从语义上讲,两种初始化的方式是一样的。
  • @MehrdadAfshari 在 Johannes 的答案代码中,根据您使用的两者中的哪一个,您会得到不同的输出。
  • @BrianGordon 是的,你是对的。它们不是等价的。我很久以前在我的编辑中提到了理查德的评论。
【解决方案3】:

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

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

【讨论】:

  • +1。不错的答案。还需要注意显式版本。顺便说一句,重要的是要注意,您不能同时拥有单个构造函数重载的 both 版本。因此,它只会在显式情况下无法编译。如果它们都编译,它们的行为必须相似。
  • 这一定是公认的答案!简短而清晰的例子。
【解决方案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
}

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

【讨论】:

    【解决方案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 ...

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

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

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

    【讨论】:

      【解决方案6】:

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

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

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

      【讨论】:

        【解决方案7】:

        关于这部分的回答:

        A c2 = 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。

        所以优化与否按照标准它们是等效的。 请注意,这与其他答案提到的内容一致。为了正确起见,仅引用标准必须说的话。

        【讨论】:

        • 您的示例的“表达式列表都没有指定多个值”。这些有什么关系?
        【解决方案8】:

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

        带有 = 的初始化被视为复制初始化。原则上,初始化器(我们从中复制的对象)的副本被放入已初始化的对象中。但是,这样的副本可能会被优化掉(省略),并且如果初始值设定项是右值,则可以使用移动操作(基于移动语义)。省略 = 使初始化显式。显式初始化称为直接初始化

        【讨论】:

          【解决方案9】:

          很多这些情况都取决于对象的实现,所以很难给你一个具体的答案。

          考虑案例

          A a = 5;
          A a(5);
          

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

          编辑:正如其他回复中提到的,第一行实际上将调用复制构造函数。将与赋值运算符相关的 cmets 视为与独立赋值相关的行为。

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

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

          【讨论】:

          • 这不是优化。在这两种情况下,编译器必须调用构造函数。因此,如果您只有operator =(const int) 而没有A(const int),它们都不会编译。有关详细信息,请参阅@jia3ep 的答案。
          • 我相信你是正确的。然而,它会通过使用默认的复制构造函数编译得很好。
          • 另外,正如我所提到的,通常的做法是让复制构造函数调用赋值运算符,此时编译器优化确实发挥了作用。
          猜你喜欢
          • 2018-10-29
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-03-16
          • 1970-01-01
          相关资源
          最近更新 更多