Is assign with braces the same as call the constructor? - c++

I know that for scalar types you can assign values with braces like int a { 0 };.
This helps with cast, type conversion ecc.
But what for udt? Is
shared_ptr<int> myIntSmartPtr { my_alloc(42), my_free };
the same as
shared_ptr<int> myIntSmartPtr = shared_ptr<int>(my_alloc(42), my_free);
The braces should call the constructor, right?
Is it like an initializer list?
I know what an std::initializer_list is, but it must be the same type T, while in { my_alloc(42), my_free } the types diverge.

This is direct list initialization.
shared_ptr<int> myIntSmartPtr { my_alloc(42), my_free };
This is an example of the first syntax:
T object { arg1, arg2, ... }; (1)
The exact effect it has is therefore
List initialization is performed in the following situations:
direct-list-initialization (both explicit and non-explicit constructors are considered)
initialization of a named variable with a braced-init-list (that is, a possibly empty brace-enclosed list of expressions or nested braced-init-lists)
And for more detail about what that actually means:
The effects of list-initialization of an object of type T are:
... [A bunch of cases that don't apply]
Otherwise, the constructors of T are considered, in two phases:
All constructors that take std::initializer_list as the only argument, or as the first argument if the remaining arguments have default values, are examined, and matched by overload resolution against a single argument of type std::initializer_list
If the previous stage does not produce a match, all constructors of T participate in overload resolution against the set of arguments that consists of the elements of the braced-init-list, with the restriction that only non-narrowing conversions are allowed. If this stage produces an explicit constructor as the best match for a copy-list-initialization, compilation fails (note, in simple copy-initialization, explicit constructors are not considered at all).
std::shared_ptr does not have a constructor that takes an std::initializer_list, so the second bullet point applies and it's constructed from the arguments therein.

Related

Double brace initialization

Which constructor should be called in the following code and why?
struct S
{
int i;
S() = default;
S(void *) : i{1} { ; }
};
S s{{}};
If I use clang (from trunk), then the second one is called.
If the second constructor is commented out, then S{{}} is still valid expression, but (I believe) move-constructor from default-constructed instance of S{} is called in the case.
Why conversion constructor has priority over the default one in the very first case?
The intention of such a combination of the constructors of S is to save its std::is_trivially_default_constructible_v< S > property, except a finite set of cases, when it should be initialized in a certain way.
If the second constructor is commented out, then S{{}} is still valid expression, but (I sure) move-constructor from default-constructed instance of S{} is called in the case.
Actually, that's not what happens. The ordering in [dcl.init.list] is:
List-initialization of an object or reference of type T is defined as follows:
— If T is an aggregate class and the initializer list has a single element of type cv U, [...]
— Otherwise, if T is a character array and [...]
— Otherwise, if T is an aggregate, aggregate initialization is performed (8.6.1).
Once you remove the S(void *) constructor, S becomes an aggregate - it has no user-provided constructor. S() = default doesn't count as user-provided because reasons. Aggregate initialization from {} will end up value-initializing the i member.
Why conversion constructor has priority over the default one in the very first case?
With the void* remaining, let's keep going down the bullet list:
— Otherwise, if the initializer list has no elements [...]
— Otherwise, if T is a specialization of std::initializer_list, [...]
— Otherwise, if T is a class type, constructors are considered. The applicable constructors are enumerated
and the best one is chosen through overload resolution (13.3, 13.3.1.7).
[over.match.list] gives us a two-phase overload resolution process:
— Initially, the candidate functions are the initializer-list constructors (8.6.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.
If the initializer list has no elements and T has a default constructor, the first phase is omitted.
S doesn't have any initializer list constructors, so we go into the second bullet and enumerate all the constructors with the argument list of {}. We have multiple viable constructors:
S(S const& );
S(S&& );
S(void *);
The conversion sequences are defined in [over.ics.list]:
Otherwise, if the parameter is a non-aggregate class X and overload resolution per 13.3.1.7 chooses a single
best constructor C of X to perform the initialization of an object of type X from the argument initializer list:
— If C is not an initializer-list constructor and the initializer list has a single element of type cv U, [...]
— Otherwise, the implicit conversion sequence is a user-defined conversion sequence with the second standard conversion sequence an identity conversion.
and
Otherwise, if the parameter type is not a class: [...] — if the initializer list has no elements, the implicit conversion sequence is the identity conversion.
That is, the S(S&& ) and S(S const& ) constructors are both user-defined conversion sequences plus identity conversion. But S(void *) is just an identity conversion.
But, [over.best.ics] has this extra rule:
However, if the target is
— the first parameter of a constructor or
— the implicit object parameter of a user-defined conversion function
and the constructor or user-defined conversion function is a candidate by
— 13.3.1.3, when [...]
— 13.3.1.4, 13.3.1.5, or 13.3.1.6 (in all cases), or
— the second phase of 13.3.1.7 when the initializer list has exactly one element that is itself an initializer list, and the target is the first parameter of a constructor of class X, and the conversion is to X or reference to (possibly cv-qualified) X,
user-defined conversion sequences are not considered.
This excludes from consideration S(S const&) and S(S&& ) as candidates - they are precisely this case - the target being the first parameter of the constructor as a result of the second phase of [over.match.list] and the target being a reference to possibly cv-qualified S, and such a conversion sequence would be user-defined.
Hence, the only remaining candidate is S(void *), so it's trivially the best viable candidate.

Why does C++ allow std::initializer_list to be coerced to primitive types, and be used to initialise them?

This question is regarding std::initializer_list, and why it is allowed to initialise primitive types. Consider the following two functions:
void foo(std::string arg1, bool arg2 = false);
void foo(std::string arg1, std::deque<std::string> arg2, bool arg3 = false);
Why is it that, when calling foo like this:
foo("some string", { });
The first overload is picked, instead of the second? Well, actually not why it's picked, it's because { } can be used to initialise anything, including primitive types. My question is the reasoning behind this.
std::initializer_list takes { args... }, and as such cannot have indeterminate length at the time of compilation. Attempting to do something like bool b = { true, true } gives error: scalar object 'b' requires one element in initialiser.
While it might have seemed like a good idea to allow uniform initialisation, the fact is that this is confusing and entirely unexpected behaviour. Indeed, how is the compiler able to do this, without some magic in the background doing std::initializer_list things?
Unless { args... } is a C++ lexical construct, in which case my point still stands: why is it allowed to be used in the initialisation of primitive types?
Thanks. I had quite the bug-hunting session here, before realising that the wrong overload was being called. Spent 10 minutes figuring out why.
That {} syntax is a braced-init-list, and since it is used as an argument in a function call, it copy-list-initializes a corresponding parameter.
§ 8.5 [dcl.init]/p17:
(17.1) — If the initializer is a (non-parenthesized) braced-init-list, the object or reference is list-initialized (8.5.4).
§ 8.5.4 [dcl.init.list]/p1:
List-initialization is initialization of an object or reference from a braced-init-list. Such an initializer is
called an initializer list, and the comma-separated initializer-clauses of the list are called the elements of the
initializer list. An initializer list may be empty. List-initialization can occur in direct-initialization or copy-initialization contexts; [...]
For a class-type parameter, with list-initialization, overload resolution looks up for a viable constructor in two phases:
§ 13.3.1.7 [over.match.list]/p1:
When objects of non-aggregate class type T are list-initialized (8.5.4), overload resolution selects the constructor
in two phases:
— 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.
but:
If the initializer list has no elements and T has a default constructor, the first phase is omitted.
Since std::deque<T> defines a non-explicit default constructor, one is added to a set of viable functions for overload resolution. Initialization through a constructor is classified as a user-defined conversion (§ 13.3.3.1.5 [over.ics.list]/p4):
Otherwise, if the parameter is a non-aggregate class X and overload resolution per 13.3.1.7 chooses a single
best constructor of X to perform the initialization of an object of type X from the argument initializer list,
the implicit conversion sequence is a user-defined conversion sequence with the second standard conversion
sequence an identity conversion.
Going further, an empty braced-init-list can value-initialize its corresponding parameter (§ 8.5.4 [dcl.init.list]/p3), which for literal types stands for zero-initialization:
(3.7) — Otherwise, if the initializer list has no elements, the object is value-initialized.
This, for literal types like bool, doesn't require any conversion and is classified as a standard conversion (§ 13.3.3.1.5 [over.ics.list]/p7):
Otherwise, if the parameter type is not a class:
(7.2) — if the initializer list has no elements, the implicit conversion sequence is the identity conversion.
[ Example:
void f(int);
f( { } );
// OK: identity conversion
— end example ]
Overload resolution checks in first place if there exists an argument for which a conversion sequence to a corresponding parameter is better than in another overload (§ 13.3.3 [over.match.best]/p1):
[...] Given these definitions, a viable function F1 is defined to be a better function than another viable function
F2 if for all arguments i, ICSi(F1) is not a worse conversion sequence than ICSi(F2), and then:
(1.3) — for some argument j, ICSj(F1) is a better conversion sequence than ICSj(F2), or, if not that, [...]
Conversion sequences are ranked as per § 13.3.3.2 [over.ics.rank]/p2:
When comparing the basic forms of implicit conversion sequences (as defined in 13.3.3.1)
(2.1) — a standard conversion sequence (13.3.3.1.1) is a better conversion sequence than a user-defined conversion sequence or an ellipsis conversion sequence, and [...]
As such, the first overload with bool initialized with {} is considered as a better match.
Unfortunately, {} does not actually indicate an std::initializer_list. It is also used for uniform initialization. Uniform initialization was intended to fix the problems of the piles of different ways C++ objects could be initialized but ended up just making things worse, and the syntactic conflict with std::initializer_list is fairly awful.
Bottom line is that {} to denote an std::initializer_list and {} to denote uniform initialization are two different things, except when they're not.
Indeed, how is the compiler able to do this, without some magic in the
background doing std::initialiser_list things?
The aforementioned magic most assuredly exists. { args... } is simply a lexical construct and the semantic interpretation depends on context- it is certainly not an std::initializer_list, unless the context says it is.
why is it allowed to be used in the initialisation of primitive types?
Because the Standards Committee did not properly consider how broken it was to use the same syntax for both features.
Ultimately, uniform init is broken by design, and should realistically be banned.
My question is the reasoning behind this.
The reasoning behind it is simple (albeit flawed). List-initialization initializes everything.
In particular, {} stands for "default" initializing the object it corresponds to; Whether this means that its initializer_list-constructor is called with an empty list, or that its default constructor is called, or that it is value-initialized, or that all of an aggregates subobjects are initialized with {}, etc. is irrelevant: It is supposed to act as a universal initializer for any object that the above can be applied to.
If you wanted to call the second overload, you'd have to pass e.g. std::deque<std::string>{} (or pass three arguments in the first place). That is the current modus operandi.
While it might have seemed like a good idea to allow uniform
initialisation, the fact is that this is confusing and entirely
unexpected behaviour.
I wouldn't call it "entirely unexpected" by any means. What is confusing about list-initializing primitive types? It is absolutely vital for aggregates - but there's not that big of a step from aggregate types to arithmetic ones, as no initializer_list is involved in both cases. Don't forget that it can e.g. be useful to prevent narrowing as well.
std::initialiser_list takes { args... }, and as such cannot have
indeterminate length at the time of compilation.
Well, technically speaking,
std::initializer_list<int> f(bool b) {
return b? std::initializer_list<int>{} : std::initializer_list<int>{1};
}

Why scalar in braces are not interpeted as initializer_list

Consider the following code snippet:
#include <iostream>
#include <initializer_list>
struct C
{
C(std::initializer_list<int>) { std::cout << "list\n"; }
C(std::initializer_list<int>, std::initializer_list<int>) { std::cout << "twice-list\n"; }
};
int main()
{
C c1 { {1,2}, {3} }; // twice-list ctor
C c2 { {1}, {2} }; // why not twice-list ?
return 0;
}
Live demo.
Why scalar values in braces for c2 variable are not interpreted as separate std::initializer_list?
First, something very important: You have two different kinds of constructors. The first in particular, C(std::initializer_list<int>), is called an initializer-list constructor. The second is just a normal user-defined constructor.
[dcl.init.list]/p2
A constructor is an initializer-list constructor if its first parameter is of type std::initializer_list<E> or reference to possibly cv-qualified std::initializer_list<E> for some type E, and either there are no other parameters or else all other parameters have default arguments (8.3.6).
In a list-initialization containing one or more initializer-clauses, initializer-list constructors are considered before any other constructors. That is, initializer-list constructors are initially the only candidates during overload resolution.
[over.match.list]/p1
When objects of non-aggregate class type T are list-initialized such that 8.5.4 specifies that overload resolution is performed according to the rules in this section, overload resolution selects the constructor in two phases:
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 for both declarations of c1 and c2 the candidate set consists only of the C(std::initializer_list<int>) constructor.
After the constructor is selected the arguments are evaluated to see if there exists an implicit conversion sequence to convert them to the parameter types. This takes us to the rules for initializer-list conversions:
[over.ics.list]/p4 (emphasis mine):
Otherwise, if the parameter type is std::initializer_list<X> and all the elements of the initializer list can be implicitly converted to X, the implicit conversion sequence is the worst conversion necessary to convert an
element of the list to X, or if the initializer list has no elements, the identity conversion.
This means a conversion exists if each element of the initializer list can be converted to int.
Let's focus on c1 for now: For the initializer-list {{1, 2}, {3}}, the initializer-clause {3} can be converted to int ([over.ics.list]/p9.1), but not {1, 2} (i.e int i = {1,2} is ill-formed). This means the condition for the above quote is violated. Since there is no conversion, overload resolution fails since there are no other viable constructors and we are taken back to the second phase of [over.match.list]/p1:
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.
Notice the change in wording at the end. The argument list in the second phase is no longer a single initializer-list, but the arguments of the braced-init-list used in the declaration. This means we can evaluate the implicit conversions in terms of the initializer-lists individually instead of at the same time.
In the initializer-list {1, 2}, both initializer-clauses can be converted to int, so the entire initializer-clause can be converted to initializer_list<int>, the same for {3}. Overload resolution is then resolved with the second constructor being chosen.
Now let's focus on c2, which should be easy now. The initializer-list constructor is first evaluated, and, using { {1}, {2} } there surely exists a conversion to int from {1} and {2}, so the first constructor is chosen.
C c2 { {1}, {2} };
This line does not pass in two arguments of std::initializer_list<int>, but rather, it is passing in one std::initializer_list<std::initializer_list<int> >. A solution would be to instead instantiate c2 like so:
C c2({1}, {2});

What language rules permit C++11 to deduce that this is an initializer_list of pairs?

In C++11, it seems like it's legal to initialize a std::map<std::string, int> as follows:
std::map<std::string, int> myMap = {
{ "One", 1 },
{ "Two", 2 },
{ "Three", 3 }
};
Intuitively, this makes sense - the brace-enclosed initializer is a list of pairs of strings, and std::map<std::string, int>::value_type is std::pair<std::string, int> (possibly with some const qualifications.
However, I'm not sure I understand how the typing works here. If we eliminate the variable declaration here and just have the brace-enclosed initializer, the compiler wouldn't know that it was looking at a std::initializer_list<std::pair<std::string, int>> because it wouldn't know that the braced pairs represented std::pairs. Therefore, it seems as though the compiler is somehow deferring the act of assigning a type to the brace-enclosed initializer until it has enough type information from the std::map constructor to realize that the nested braces are for pairs. I don't remember anything like this happening in C++03; to the best of my knowledge, the type of an expression never depended on its context.
What language rules permit this code to compile correctly and for the compiler to determine what type to use for the initializer list? I'm hoping for answers with specific references to the C++11 spec, since it's really interesting that this works!
Thanks!
In the expression
std::map<std::string, int> myMap = {
{ "One", 1 },
{ "Two", 2 },
{ "Three", 3 }
};
on the right side you have a braced-init-list where each element is also a braced-init-list. The first thing that happens is that the initializer list constructor of std::map is considered.
map(initializer_list<value_type>,
const Compare& = Compare(),
const Allocator& = Allocator());
map<K, V>::value_type is a typedef for pair<const K, V>, in this case pair<const string, int>. The inner braced-init-lists can be successfully converted to map::value_type because std::pair has a constructor that takes references to its two constituent types, and std::string has an implicit conversion constructor that takes a char const *.
Thus the initializer list constructor of std::map is viable, and the construction can happen from the nested braced-init-lists.
The relevant standardese is present in §13.3.1.7/1 [over.match.list]
When objects of non-aggregate class type T are list-initialized (8.5.4), overload resolution selects the constructor in two phases:
— 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.
The first bullet is what causes the initializer_list constructor of map to be selected for the outer braced-init-list, while the second bullet results in the selection of the correct pair constructor for the inner braced-init-lists.
This is list-initialization. The rules are found in §8.5.4[dcl.init.list]/p3 of the standard:
List-initialization of an object or reference of type T is defined as
follows:
If the initializer list has no elements and T is a class type with a default constructor, the object is value-initialized.
Otherwise, if T is an aggregate, aggregate initialization is performed (8.5.1). [example omitted]
Otherwise, if T is a specialization of std::initializer_list<E>, an initializer_list object is constructed as described below and
used to initialize the object according to the rules for
initialization of an object from a class of the same type (8.5).
Otherwise, if T is a class type, constructors are considered. The applicable constructors are enumerated and the best one is chosen
through overload resolution (13.3, 13.3.1.7). If a narrowing
conversion (see below) is required to convert any of the arguments,
the program is ill-formed.
[example and remainder of the rules omitted]
Note that overload resolution will prefer std::initializer_list constructors in these cases (§13.3.1.7 [over.match.list]).
Thus when the compiler sees an braced list used to initialize a object of a non-aggregate, non-std::initializer_list class type, it will perform overload resolution to select the appropriate constructor, preferring the initializer_list constructor if a viable one exists (as it does for std::map).

Is it possible to invoke a user-defined conversion function via list-initialization?

Is this program legal?
struct X { X(const X &); };
struct Y { operator X() const; };
int main() {
X{Y{}}; // ?? error
}
After n2672, and as amended by defect 978, 13.3.3.1 [over.best.ics] has:
4 - However, when considering the argument of a constructor or user-defined conversion function that is a candidate [...] by 13.3.1.7 [...] when the initializer list has exactly one element and a conversion to some class X or reference to (possibly cv-qualified) X is considered for the first parameter of a constructor of X [...], only standard conversion sequences and ellipsis conversion sequences are considered.
This seems rather perverse; it has the result that specifying a conversion using a list-initialization cast is illegal:
void f(X);
f(Y{}); // OK
f(X{Y{}}); // ?? error
As I understand n2640, list-initialization is supposed to be able to replace all uses of direct-initialization and copy-initialization, but there seems no way to construct an object of type X from an object of type Y using only list-initialization:
X x1(Y{}); // OK
X x2 = Y{}; // OK
X x3{Y{}}; // ?? error
Is this the actual intent of the standard; if not, how should it read or be read?
The original intent of 13.3.3.1p4 is to describe how to apply the requirement in 12.3p4 that:
4 - At most one user-defined conversion (constructor or conversion function) is implicitly applied to a single value.
Before defect 84, 13.3.3.1p4 was almost purely informative:
4 - In the context of an initialization by user-defined conversion (i.e., when considering the argument of a user-defined conversion function; see 13.3.1.4 [over.match.copy], 13.3.1.5 [over.match.conv]), only standard conversion sequences and ellipsis conversion sequences are allowed.
This is because 13.3.1.4 paragraph 1 bullet 2 and 13.3.1.5p1b1 restrict the candidate functions to those on class S yielding type T, where S is the class type of the initializer expression and T is the type of the object being initialized, so there is no latitude for another user-defined conversion conversion sequence to be inserted. (13.3.1.4p1b1 is another matter; see below).
Defect 84 repaired the auto_ptr loophole (i.e. auto_ptr<Derived> -> auto_ptr<Base> -> auto_ptr_ref<Base> -> auto_ptr<Base>, via two conversion functions and a converting constructor) by restricting the conversion sequences allowable for the single parameter of the constructor in the second step of class copy-initialization (here the constructor of auto_ptr<Base> taking auto_ptr_ref<Base>, disallowing the use of a conversion function to convert its argument from auto_ptr<Base>):
4 - However, when considering the argument of a user-defined conversion function that is a candidate by 13.3.1.3 [over.match.ctor] when invoked for the copying of the temporary in the second step of a class copy-initialization, or by 13.3.1.4 [over.match.copy], 13.3.1.5 [over.match.conv], or 13.3.1.6 [over.match.ref] in all cases, only standard conversion sequences and ellipsis conversion sequences are allowed.
n2672 then adds:
[...] by 13.3.1.7 [over.match.list] when passing the initializer list as a single argument or when the initializer list has exactly one element and a conversion to some class X or reference to (possibly cv-qualified) X is considered for the first parameter of a constructor of X, [...]
This is clearly confused, as the only conversions that are a candidate by 13.3.1.3 and 13.3.1.7 are constructors, not conversion functions. Defect 978 corrects this:
4 - However, when considering the argument of a constructor or user-defined conversion function [...]
This also makes 13.3.1.4p1b1 consistent with 12.3p4, as it otherwise would allow unlimited application of converting constructors in copy-initialization:
struct S { S(int); };
struct T { T(S); };
void f(T);
f(0); // copy-construct T by (convert int to S); error by 12.3p4
The issue is then what the language referring to 13.3.1.7 means. X is being copy or move constructed so the language is excluding applying a user-defined conversion to arrive at its X argument. std::initializer_list has no conversion functions so the language must be intended to apply to something else; if it isn't intended to exclude conversion functions, it must exclude converting constructors:
struct R {};
struct S { S(R); };
struct T { T(const T &); T(S); };
void f(T);
void g(R r) {
f({r});
}
There are two available constructors for the list-initialization; T::T(const T &) and T::T(S). By excluding the copy constructor from consideration (as its argument would need to be converted via a user-defined conversion sequence) we ensure that only the correct T::T(S) constructor is considered. In the absence of this language the list-initialization would be ambiguous. Passing the initializer list as a single argument works similarly:
struct U { U(std::initializer_list<int>); };
struct V { V(const V &); V(U); };
void h(V);
h({{1, 2, 3}});
Edit: and having gone through all that, I've found a discussion by Johannes Schaub that confirms this analysis:
This is intended to factor out the copy constructor for list initialization
because since we are allowed to use nested user defined conversions, we
could always produce an ambiguous second conversion path by first invoking
the copy constructor and then doing the same as we did for the other
conversions.
OK, off to submit a defect report. I'm going to propose splitting up 13.3.3.1p4:
4 - However, when considering the argument of a constructor or user-defined conversion function that is a candidate:
by 13.3.1.3 [over.match.ctor] when invoked for the copying of the temporary in the second step of a class copy-initialization, or
by 13.3.1.4 [over.match.copy], 13.3.1.5 [over.match.conv], or 13.3.1.6 [over.match.ref] in all cases,
only standard conversion sequences and ellipsis conversion sequences are considered; when considering the first argument of a constructor of a class X that is a candidate by 13.3.1.7 [over.match.list] when passing the initializer list as a single argument or when the initializer list has exactly one element, a user-defined conversion to X or reference to (possibly cv-qualified) X is only considered if its user-defined conversion is specified by a conversion function. [Note: because more than one user-defined conversion is allowed in an implicit conversion sequence in the context of list-initialization, this restriction is necessary to ensure that a converting constructor of X, called with a single argument a that is not of type X or a type derived from X, is not ambiguous against a constructor of X called with a temporary X object itself constructed from a. -- end note]
The version of clang 3.1 shipped with XCode 4.4 agrees with your interpretation and rejects X{Y{}};. As do I, after re-reading the relevant parts of the standard a few times, FWIW.
If I modify X's constructor to take two arguments, both of type const X&, clang accepts the statement Y y; X{y,y}. (It crashes if I try X{Y{},Y{}}...). This seems to be consistent with 13.3.3.1p4 which demands user-defined conversions to be skipped only for the single-element case.
It seems that the restriction to standard and ellipsis conversion sequences was added initially only in cases where another user-defined conversion has already taken place. Or at least that is how I read http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#84.
It's interesting how the standard is careful to apply the restriction only to the second step of copy initialization, which copies from a temporary which already has the correct type (and was obtain potentially through a user-defined conversion!). Yet for list-initialization, no similar mechanism seems to exists...