Is There a Difference Between Copy Initialization and Direct Initialization

Is there a difference between copy initialization and direct initialization?

C++17 Update

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

As examples in our case, in the case of a1 and a2 special rules say that in such declarations, the result object of a prvalue initializer of the same type as a1 is variable a1, and therefore A_factory_func() directly initializes the object a1. Any intermediary functional-style cast would not have any effect, because A_factory_func(another-prvalue) just "passes through" the result object of the outer prvalue to be also the result object of the inner prvalue.


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

Depends on what type A_factory_func() returns. I assume it returns an A - then it's doing the same - except that when the copy constructor is explicit, then the first one will fail. Read 8.6/14

double b1 = 0.5;
double b2(0.5);

This is doing the same because it's a built-in type (this means not a class type here). Read 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).


Delving into Initializations Direct and Copy initialization

While they look identical and are supposed to do the same, these two forms are remarkably different in certain cases. The two forms of initialization are direct and copy initialization:

T t(x);
T t = x;

There is behavior we can attribute to each of them:

  • 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)

As you see, copy initialization is in some way a part of direct initialization with regard to possible implicit conversions: While direct initialization has all constructors available to call, and in addition can do any implicit conversion it needs to match up argument types, copy initialization can just set up one implicit conversion sequence.

I tried hard and got the following code to output different text for each of those forms, without using the "obvious" through explicit constructors.

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

How does it work, and why does it output that result?

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

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

I hope this helps somewhat to make it clearer how these two forms differ!

Difference copy-initialization and direct-initialization for primitive (scalar) types [duplicate]

There is no difference for primitive scalars like this; the memory location or register (depending on usage) is going to be initialized the same way.

Is it direct-initialization or copy-initialization?

obj s = obj("value");

This is direct initialization of a prvalue, which is then used to copy initialize the variable s. C++17's prvalue rules make this de-facto direct initialization of s.

obj s{"value"};

This is direct-list-initialization. The "list" part is important. Anytime you apply a braced-init-list for the purposes of initializing an object, you are performing list-initialization.

obj s = {"value"};

This is copy-list-initialization.

obj s = obj{"value"};

This is direct-list-initialization of a prvalue, which is then used to copy initialize the variable s.

obj s("value");

That is direct initialization.

obj s = "value";

That's copy initialization.

Mr. Stroustrup presents these different initialization styles as equal.

They are equal in the sense that they do mostly the same thing. But they're not technically equal; copy-list-initialization cannot call explicit constructors. So if the selected constructor were explicit, the code would fail to compile in the copy-list-initialization cases.

Make difference between copy and direct initialization

I believe it's impossible in general, but if you only want to support the two ways of initialization you listed, there are hacky solutions.


You need two constructors, one explicit and the other non-explicit. As you were already told in comments, operator= won't help you, since both lines perform initialization and not assignment.

However, if the there are no other differences between the two constructors, the code won't compile.

You need to make the explicit constructor "better" than its non-explicit counterpart, so that it will be preferred if possible (i.e. for direct-initialization), so the other is used as a fallback.

Figuring out how exactly to make one constructor "better" than the other is left as an exercise to the reader. There are at least three approaches:

  • Using lvalue references vs rvalue references.
  • Using ....
  • Using a helper class with a constructor with an int parameter.

Differences between direct-list-initialization and copy-list-initialization

List initialization is informally called "uniform initialization" because its meaning and behavior is intended to be the same regardless of how you invoke it.

Of course, C++ being C++, what is "intended" doesn't always happen.

There are basically three major differences between the behavior of direct-list-initialization and copy-list-initialization. The first is the one you will encounter most frequently: if list initialization would call a constructor marked explicit, then there is a compile error if the list-initialization form is copy-list-initialization.

This behavioral difference is defined in [over.match.list]/1:

In copy-list-initialization, if an explicit constructor is chosen, the initialization is ill-formed.

That's a function of overload resolution.

The second major difference (new to C++17) is that, given an enum-type with a fixed underlying size, you can perform direct-list-initialization on it with a value of the underlying type. But you cannot perform copy-list-initialization from such a value. So enumeration e{value}; works, but not enumeration e = {value};.

The third major difference (also new to C++17) relates to the behavior of braced-init-lists in auto deduction. Normally, auto behaves not too distinctly from template argument deduction. But unlike template argument deduction, auto can be initialized from a braced-init-list.

If you initialize an auto variable using direct-list-initialization with a single expression in the list, the compiler will deduce the variable to be of the type of the expression:

auto x{50.0f}; //x is a `float`.

Sounds reasonable. But if do the exact same thing with copy-list-initialization, it will always be deduced to an initializer_list<T>, where T is the type of the initializer:

auto x = {50.0f}; //x is an `initializer_list<float>`

So very, very uniform. ;)

Thankfully, if you use multiple initializers in the braced-init-list, direct-list-initialization for an auto-deduced variable will always give a compile error, while copy-list-initialization will just give a longer initializer_list. So auto-deduced direct-list-initialization will never give an initializer_list, while auto-deduced copy-list-initialization always will.

There are some minor differences that rarely affects the expected behavior of the initialization. These are cases where list-initialization from a single value will use copy or direct (non-list) initialization as appropriate to the list-initialization form. These cases are:

  1. Initializing an aggregate from a single value which is the same type as the aggregate being initialized. This bypasses aggregate initialization.

  2. Initializing a non-class, non-enumeration type from a single value.

  3. Initializing a reference.

Not only do these not happen particularly frequently, they basically never really change the meaning of the code. Non-class types don't have explicit constructors, so the difference between copy and direct initialization is mostly academic. Same goes for references. And the aggregate case is really just about performing a copy/move from a given value.



Related Topics



Leave a reply



Submit