Why does the standard differentiate between direct-list-initialization and copy-list-initialization?
Asked Answered
C

1

41

We know that T v(x); is called direct-initialization, while T v = x; is called copy-initialization, meaning that it will construct a temporary T from x that will get copied / moved into v (which is most likely elided).

For list-initialization, the standard differentiates between two forms, depending on the context. T v{x}; is called direct-list-initialization while T v = {x}; is called copy-list-initialization:

§8.5.4 [dcl.init.list] p1

[...] List-initialization can occur in direct-initialization or copy-initialization contexts; list-initialization in a direct-initialization context is called direct-list-initialization and list-initialization in a copy-initialization context is called copy-list-initialization. [...]

However, there are only two more references each in the whole standard. For direct-list-initialization, it's mentioned when creating temporaries like T{x} (§5.2.3/3). For copy-list-initialization, it's for the expression in return statements like return {x}; (§6.6.3/2).

Now, what about the following snippet?

#include <initializer_list>

struct X{
  X(X const&) = delete; // no copy
  X(X&&) = delete; // no move
  X(std::initializer_list<int>){} // only list-init from 'int's
};

int main(){
  X x = {42};
}

Normally, from the X x = expr; pattern, we expect the code to fail to compile, because the move constructor of X is defined as deleted. However, the latest versions of Clang and GCC compile the above code just fine, and after digging a bit (and finding the above quote), that seems to be correct behaviour. The standard only ever defines the behaviour for the whole of list-initialization, and doesn't differentiate between the two forms at all except for the above mentioned points. Well, atleast as far as I can see, anyways.

So, to summarize my question again:

What is the use of splitting list-initialization into its two forms if they (apparently) do the exact same thing?

Commodity answered 19/11, 2012 at 19:47 Comment(2)
If you made that third constructor explicit, would it stop working?Wassail
The difference between direct-initialization and copy-initialization is defined 8.5 #16. The direct-initialization considers only constructors, whereas copy-initialization considers user-defined conversion sequences. Thus making the ctor explicit will result in a compiler error.Centistere
F
37

Because they don't do the exact same thing. As stated in 13.3.1.7 [over.match.list]:

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

In short, you can only use implicit conversion in copy-list-initialization contexts.

This was explicitly added to make uniform initialization not, um, uniform. Yeah, I know how stupid that sounds, but bear with me.

In 2008, N2640 was published (PDF), taking a look at the current state of uniform initialization. It looked specifically at the difference between direct initialization (T{...}) and copy-initialization (T = {...}).

To summarize, the concern was that explicit constructors would effectively become pointless. If I have some type T that I want to be able to be constructed from an integer, but I don't want implicit conversion, I label the constructor explicit.

Then somebody does this:

T func()
{
  return {1};
}

Without the current wording, this will call my explicit constructor. So what good is it to make the constructor explicit if it doesn't change much?

With the current wording, you need to at least use the name directly:

T func()
{
  return T{1};
}
Fulmer answered 19/11, 2012 at 20:10 Comment(6)
I should've known this, since I constantly complain about std::tuple and std::unique_ptrs constructor being explicit so I can't do return {a, b, c};. :)Commodity
Well, since uniform initialization doesn't work uniformly on all types, I think the phrase "make uniform initialization not, um, uniform" is actually void. Eg. in snippet X x; X y{x};, y is a copy of x for all copyable types except those that have at least one list-constructor from any type (for those types, it usually doesn't compile). So in templates, you have to resort back to old C++03 initialization if you want to copy or convert.Pelaga
@jpalecek: That's not true. The initializer_list constructor will only be used if the given braced-init-list is of the same type as the constructor. Therefore, it would only be called if X had a X(initializer_list<X>) constructor, which is rather unlikely. And even if it did, wouldn't it just... copy the member(s)? I'm not sure what it is you're concerned about here. The concern is usually for things like vector<int>, where the constructor that takes a size_t conflicts with the initailizer_list constructor.Fulmer
@NicolBolas: You are wrong. The mere presence of any initializer_list constructor means any other constructors are not considered if you used the braced syntax. The standard says it clearly: "If T has an initializer-list constructor, the argument list consists of the initializer list as a single argument; otherwise, the argument list consists of the elements of the initializer list. The applicable constructors are enumerated ... " and that's it. See it in action: ideone.com/z56OES. So you can't use the braces for types that by chance might have the list constructor if you mean copy.Pelaga
@Pelaga I believe that is a gcc4.5.1 bug, as this compiles on 4.7.0. b is being initialized via the (compiler generated) copy constructor, and the constructor taking initializer_list is not selected, even though it is preferred, because the member(s) of the braced-init-list are not convertible to int. As Nicol says in the earlier comment, if there existed a A::A(initializer_list<A>) constructor, that would be selected over the copy constructor.Gaskin
@jpalecek: From section 13.3.1.7, p1: -Initially, the candidate functions are the initializer-list constructors (8.5.4) of the class T and the argument list consists of the initializer list as a single argument. - If no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all the constructors of the class T and the argument list consists of the elements of the initializer list. So you must be looking at an old version of the standard (I couldn't find that statement anywhere).Fulmer

© 2022 - 2024 — McMap. All rights reserved.