假设我有这样一个函数:

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

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

我见过有人两种说法都说。请引用原文作为证明。也请添加其他案例。


赋值不同于初始化。

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

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

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

考虑一下这个案例

A a = 5;
A a(5);

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

编辑:正如在其他响应中提到的,第一行实际上将调用复制构造函数。将与赋值操作符相关的注释视为与独立赋值相关的行为。

也就是说,编译器如何优化代码会有它自己的影响。如果初始化构造函数调用“=”运算符——如果编译器不做任何优化,则顶部行将执行两次跳转,而底部行则执行一次跳转。

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


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

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

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


First grouping: it depends on what A_factory_func returns. The first line is an example of copy initialization, the second line is direct initialization. If A_factory_func returns an A object then they are equivalent, they both call the copy constructor for A, otherwise the first version creates an rvalue of type A from an available conversion operators for the return type of A_factory_func or appropriate A constructors, and then calls the copy constructor to construct a1 from this temporary. The second version attempts to find a suitable constructor that takes whatever A_factory_func returns, or that takes something that the return value can be implicitly converted to.

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

Third grouping: c1 is default initialized, c2 is copy-initialized from a value initialized temporary. Any members of c1 that have pod-type (or members of members, etc., etc.) may not be initialized if the user supplied default constructors (if any) do not explicitly initialize them. For c2, it depends on whether there is a user supplied copy constructor and whether that appropriately initializes those members, but the members of the temporary will all be initialized (zero-initialized if not otherwise explicitly initialized). As litb spotted, c3 is a trap. It's actually a function declaration.


c++ 17更新

In C++17, the meaning of A_factory_func() changed from creating a temporary object (C++<=14) to just specifying the initialization of whatever object this expression is initialized to (loosely speaking) in C++17. These objects (called "result objects") are the variables created by a declaration (like a1), artificial objects created when the initialization ends up being discarded, or if an object is needed for reference binding (like, in A_factory_func();. In the last case, an object is artificially created, called "temporary materialization", because A_factory_func() doesn't have a variable or reference that otherwise would require an object to exist).

以我们的例子为例,在a1和a2的情况下,特殊的规则说,在这样的声明中,与a1相同类型的prvalue初始化器的结果对象是变量a1,因此A_factory_func()直接初始化对象a1。任何中间函数风格的强制转换都不会产生任何影响,因为A_factory_func(另一个-prvalue)只是将外部prvalue的结果对象“传递”为内部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());

This is not doing the same. The first default-initializes if A is a non-POD, and doesn't do any initialization for a POD (Read 8.6/9). The second copy initializes: Value-initializes a temporary and then copies that value into c2 (Read 5.2.3/2 and 8.6/14). This of course will require a non-explicit copy constructor (Read 8.6/14 and 12.3.1/3 and 13.3.1.3/1 ). The third creates a function declaration for a function c3 that returns an A and that takes a function pointer to a function returning a A (Read 8.2).


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

虽然它们看起来一模一样,而且应该做同样的事情,但在某些情况下,这两种形式有显著的不同。初始化有两种形式:直接初始化和复制初始化:

T t(x);
T t = x;

我们可以把它们各自的行为归结为:

Direct initialization behaves like a function call to an overloaded function: The functions, in this case, are the constructors of T (including explicit ones), and the argument is x. Overload resolution will find the best matching constructor, and when needed will do any implicit conversion required. Copy initialization constructs an implicit conversion sequence: It tries to convert x to an object of type T. (It then may copy over that object into the to-initialized object, so a copy constructor is needed too - but this is not important below)

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

我努力尝试并得到了以下代码,为每一种形式输出不同的文本,而不使用“明显的”通过显式构造函数。

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

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

Direct initialization It first doesn't know anything about conversion. It will just try to call a constructor. In this case, the following constructor is available and is an exact match: B(A const&) There is no conversion, much less a user defined conversion, needed to call that constructor (note that no const qualification conversion happens here either). And so direct initialization will call it. Copy initialization As said above, copy initialization will construct a conversion sequence when a has not type B or derived from it (which is clearly the case here). So it will look for ways to do the conversion, and will find the following candidates B(A const&) operator B(A&); Notice how I rewrote the conversion function: The parameter type reflects the type of the this pointer, which in a non-const member function is to non-const. Now, we call these candidates with x as argument. The winner is the conversion function: Because if we have two candidate functions both accepting a reference to the same type, then the less const version wins (this is, by the way, also the mechanism that prefers non-const member function calls for non-const objects). Note that if we change the conversion function to be a const member function, then the conversion is ambiguous (because both have a parameter type of A const& then): The Comeau compiler rejects it properly, but GCC accepts it in non-pedantic mode. Switching to -pedantic makes it output the proper ambiguity warning too, though.


值得注意的是:

[12.2/1]类类型的临时对象可以在不同的上下文中创建:…在一些初始化中(8.5)。

例如,用于复制初始化。

[12.8/15]当满足某些条件时,允许实现省略类对象的复制构造…

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

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

技术血淋淋的景象: [12.2/1接续以上]即使避免了临时对象的创建(12.8),也必须遵守所有的语义限制,就像创建了临时对象一样。

很高兴我不是在写c++编译器。


就这部分回答:

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

由于大多数答案都是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.

所以不管优化与否,它们在标准上是等价的。 请注意,这与前面提到的其他答案是一致的。只是为了正确起见,引用了标准的内容。


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

类:

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

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

带有=的初始化被认为是复制初始化。原则上,初始化器(我们要从中复制的对象)的副本被放置到初始化的对象中。但是,这样的副本可以被优化掉(省略),如果初始化式是右值,则可以使用move操作(基于move语义)。省略=可以显式地初始化。显式初始化称为直接初始化。