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

rbpvctlc  于 2022-12-24  发布在  其他
关注(0)|答案(9)|浏览(181)

假设我有这个函数:

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

在每一组中,这些语句是相同的吗?或者在一些初始化中有额外的(可能是可优化的)副本吗?
我看到有人说这两件事。请引用文本作为证明。也请添加其他情况。

f3temu5u

f3temu5u1#

C++17更新

在C17中,A_factory_func()的含义从创建临时对象更改为(C〈=14)设置为只指定此表达式初始化到的任何对象的初始化(不严格地说)在C++17中。这些对象(称为“结果对象”)是由声明创建的变量(如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-那么它做的是一样的-除了当复制构造函数是显式的,那么第一个将失败。Read 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)。第二个副本初始化:Value-初始化一个临时变量,然后将该值复制到c2(阅读5.2.3/2和8.6/14),当然这需要一个非显式的复制构造函数(阅读8.6/14和12.3.1/3以及13.3.1.3/1)。第三个代码为函数c3创建函数声明,该函数返回A,并接受指向返回A的函数的函数指针(Read 8.2)。

深入了解初始化直接和拷贝初始化

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

T t(x);
T t = x;

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

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

如您所见,copy initialization 在某种程度上是直接初始化的一部分,与可能的隐式转换有关:直接初始化有所有的构造函数可供调用,而且 * 此外 * 还可以做任何它需要匹配参数类型的隐式转换,而复制初始化只能设置一个隐式转换序列。
我努力尝试了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&)

调用构造函数不需要转换,更不用说用户定义的转换了(注意这里也没有发生常量限定转换),所以直接初始化将调用它。
1.复制初始化
如上所述,当a没有类型B或从B派生时(这里的情况很明显),复制初始化将构造转换序列,因此它将寻找进行转换的方法,并将找到以下候选项

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

注意我是如何重写转换函数的:参数类型反映了this指针的类型,在非常数成员函数中是指向非常数的。现在,我们用x作为参数调用这些候选对象。赢家的是转换函数:因为如果我们有两个候选函数都接受对同一类型的引用,那么 less const 版本会胜出(顺便说一下,这也是一种机制,它更喜欢非常数成员函数调用非常数对象)。
注意,如果我们将转换函数更改为const成员函数,则转换是不明确的(因为两者都有A const&的参数类型):Comeau编译器正确地拒绝了它,但GCC以非学究式的方式接受了它,不过切换到-pedantic也会输出正确的歧义警告。

cbeh67ev

cbeh67ev2#

分配初始化不同。

下面两行都执行 * 初始化 *,只执行一次构造函数调用:

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

ldxq2e6h3#

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

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

pengsaosao

pengsaosao4#

当你初始化一个对象时,你可以看到它在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
baubqpgj

baubqpgj5#

注意:
[12.2/1] x一个月一个月
即用于复制初始化。
[12.8/15] x一米一英寸
换句话说,一个好的编译器在可以避免复制初始化的时候,不会为复制初始化创建一个副本;相反,它将直接调用构造函数--也就是说,就像直接初始化一样。
In other words, copy-initialization is just like direct-initialization in most cases where understandable code has been written. Since direct-initialization potentially causes arbitrary (and therefore probably unknown) conversions, I prefer to always use copy-initialization when possible. (With the bonus that it actually looks like initialization.)
技术精湛:[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++编译器。

byqmnocz

byqmnocz6#

第一组:这取决于A_factory_func返回什么。第一行是 * 复制初始化 * 的示例,第二行是 * 直接初始化 *。如果A_factory_func返回A对象,则它们是等效的,它们都调用A的复制构造函数,否则第一版本从用于A_factory_func的返回类型的可用转换运算符或适当的A构造函数创建A类型的右值,然后调用复制构造函数从这个临时值构造a1,第二个版本试图找到一个合适的构造函数,它接受A_factory_func返回的任何内容,或者接受返回值可以隐式转换成的内容。
第二组:完全相同的逻辑成立,除了内置类型没有任何外来的构造函数,因此它们实际上是相同的。
第三组:c1是默认初始化的,c2是从临时初始化的值复制初始化的。c1的任何具有pod类型的成员(或成员的成员等)可能无法初始化,如果用户提供了默认构造函数(如果有的话)不显式初始化它们。对于c2,这取决于是否有用户提供的复制构造函数,以及它是否适当地初始化了这些成员,但临时变量的成员都将被初始化(如果没有显式初始化,则初始化为零)正如litb所指出的,c3是一个陷阱,它实际上是一个函数声明。

jtoj6r0c

jtoj6r0c7#

关于本部分的回答:
A c2 = A(); A c3(A());
由于大多数答案都是c11之前的版本,我在这里补充一下c11对这个问题的看法:
简单类型说明符(7.1.6.2)或类型名称说明符公式14.6后面跟着一个括号中的表达式列表构造一个给定类型的值。如果表达式列表是一个单一的表达式,那么类型转换表达式是等价的(在定义中,如果在含义中定义)转换为相应的强制转换表达式(5.4)如果指定的类型是类类型,则类类型应该是完整的。**如果表达式列表指定了多个值,则类型应该是具有适当声明的构造函数的类(8.5,12.1),并且表达式T(x1,x2,...)在效果上等效于声明T t(x1,x2,...);**对于某个虚构的临时变量t,其结果是t的值作为纯右值。
因此,优化与否,根据标准是等效的。注意,这是根据其他答案所提到的。只是引用标准所要说的话,以确保正确性。

tp5buhyn

tp5buhyn8#

这是来自Bjarne Stroustrup的C++编程语言:
带有=的初始化被认为是 * 复制初始化 *。原则上,初始化式的副本(我们要从中复制的对象)被放入初始化对象中。但是,这样的副本可能会被优化掉(省略),和移动操作(基于移动语义)如果初始化式是一个右值,则可以使用。省略=会使初始化显式。显式初始化称为 * 直接初始化 *。

xqkwcwgp

xqkwcwgp9#

许多这样的情况都取决于对象的实现,因此很难给予具体的答案。
考虑一下这个案例

A a = 5;
A a(5);

在这种情况下,假设一个正确的赋值操作符和初始化构造函数接受一个整型参数,我如何实现上述方法会影响每一行的行为。然而,常见的做法是其中一个在实现中调用另一个,以消除重复代码(尽管在如此简单的情况下,没有真实的的目的)。
编辑:正如在其他回复中提到的,第一行实际上会调用复制构造函数。将与赋值运算符相关的注解视为独立赋值的行为。
也就是说,编译器如何优化代码会有自己的影响,如果我让初始化构造函数调用“=”操作符--如果编译器没有进行优化,那么顶行将执行2次跳转,而底行执行1次跳转。
现在,对于最常见的情况,你的编译器将通过这些情况进行优化,并消除这种类型的低效率。所以有效地,你描述的所有不同的情况将变成相同的。如果你想确切地看到正在做什么,你可以看看你的编译器的目标代码或汇编输出。

相关问题