I am having trouble understanding why the following copy-initialization doesn't compile:
#include <memory>
struct base{};
struct derived : base{};
struct test
{
test(std::unique_ptr<base>){}
};
int main()
{
auto pd = std::make_unique<derived>();
//test t(std::move(pd)); // this works;
test t = std::move(pd); // this doesn't
}
A unique_ptr<derived> can be moved into a unique_ptr<base>, so why does the second statement work but the last does not? Are non-explicit constructors not considered when performing a copy-initialization?
The error from gcc-8.2.0 is:
conversion from 'std::remove_reference<std::unique_ptr<derived, std::default_delete<derived> >&>::type'
{aka 'std::unique_ptr<derived, std::default_delete<derived> >'} to non-scalar type 'test' requested
and from clang-7.0.0 is
candidate constructor not viable: no known conversion from 'unique_ptr<derived, default_delete<derived>>'
to 'unique_ptr<base, default_delete<base>>' for 1st argument
Live code is available here.
A std::unique_ptr<base> is not the same type as a std::unique_ptr<derived>. When you do
test t(std::move(pd));
You call std::unique_ptr<base>'s conversion constructor to convert pd into a std::unique_ptr<base>. This is fine as you are allowed a single user defined conversion.
In
test t = std::move(pd);
You are doing copy initialization so so you need to convert pd into a test. That requires 2 user defined conversions though and you can't do that. You first have to convert pd to a std::unique_ptr<base> and then you need to convert it to a test. It's not very intuitive but when you have
type name = something;
whatever something is needs to be only a single user defined conversion from the source type. In your case that means you need
test t = test{std::move(pd)};
which only uses a single implicit user defined like the first case does.
Lets remove the std::unique_ptr and look at in a general case. Since std::unique_ptr<base> is not the same type as a std::unique_ptr<derived> we essentially have
struct bar {};
struct foo
{
foo(bar) {}
};
struct test
{
test(foo){}
};
int main()
{
test t = bar{};
}
and we get the same error because we need to go from bar -> foo -> test and that has one user defined conversion too many.
The semantics of initializers are described in [dcl.init] ¶17. The choice of direct initialization vs copy initialization takes us into one of two different bullets:
If the destination type is a (possibly cv-qualified) class type:
[...]
Otherwise, if the initialization is direct-initialization, or if it is copy-initialization where the cv-unqualified version of the source
type is the same class as, or a derived class of, the class of the
destination, constructors are considered. The applicable constructors
are enumerated ([over.match.ctor]), and the best one is chosen through
overload resolution. The constructor so selected is called to
initialize the object, with the initializer expression or
expression-list as its argument(s). If no constructor applies, or the
overload resolution is ambiguous, the initialization is ill-formed.
Otherwise (i.e., for the remaining copy-initialization cases), user-defined conversion sequences that can convert from the source
type to the destination type or (when a conversion function is used)
to a derived class thereof are enumerated as described in
[over.match.copy], and the best one is chosen through overload
resolution. If the conversion cannot be done or is ambiguous, the
initialization is ill-formed. The function selected is called with the
initializer expression as its argument; if the function is a
constructor, the call is a prvalue of the cv-unqualified version of
the destination type whose result object is initialized by the
constructor. The call is used to direct-initialize, according to the
rules above, the object that is the destination of the
copy-initialization.
In the direct initialization case, we enter the first quoted bullet. As detailed there, constructors are considered and enumerated directly. The implicit conversion sequence that is required is therefore only to convert unique_ptr<derived> to a unique_ptr<base> as a constructor argument.
In the copy initialization case, we are not directly considering constructors anymore, but rather trying to see which implicit conversion sequence is possible. The only one available is unique_ptr<derived> to a unique_ptr<base> to a test. Since an implicit conversion sequence can contain only one user defined conversion, this is not allowed. As such the initialization is ill-formed.
One could say that using direct initialization sort of "bypasses" one implicit conversion.
Pretty sure that only single implicit conversion is allowed to be considered by the compiler. In first case only conversion from std::unique_ptr<derived>&& to std::unique_ptr<base>&& is required, in the second case the base pointer would also need to be converted to test (for default move constructor to work).
So for example converting the derived pointer to base: std::unique_ptr<base> bd = std::move(pd) and then move assigning it would work as well.
Related
I wonder which compiler is compliant with the standard, i use the following code
#include <iostream>
#include <string>
#include <memory>
#include <vector>
class AbstractBase
{
public:
virtual ~AbstractBase() {};
virtual std::string get_name() = 0;
virtual int get_number() = 0;
};
class BaseImpl : public AbstractBase
{
public:
BaseImpl() = delete; //(a)
//BaseImpl(BaseImpl&) = delete; //(b)
BaseImpl(const std::vector<std::string>& name_) : name(name_) {}
std::string get_name() override {return name.empty() ? std::string("empty") : name.front();}
private:
std::vector<std::string> name{};
};
class impl : public BaseImpl
{
public:
impl() : BaseImpl({}) {}
int get_number() override {return 42;}
};
int main()
{
std::unique_ptr<AbstractBase> intance = std::make_unique<impl>();
std::cout << intance->get_name() << " " << intance->get_number() << "\n";
return 0;
}
msvc and clang produce a compiler error while gcc is fine with this code. Link to godbold
https://godbolt.org/z/ETYGn5T1h
If one explicit delete the copy ctor of BaseImpl (uncomment (b)) all three compiler are fine. Also if one do not explicitly delete (a) the standard ctor (then no standard ctor is generated because there is an user defined ctor), all compilers are fine.
Apparently clang and msvc believe that by : BaseImpl({}) they can either call the user defined ctor, or indirectly via the default ctor the implicit copy ctor. However, since the default ctor is deleted, this ambiguity should not exist at all. However, by explicitly deleting the default ctor, the compilers seem to first assume that a default ctor exists for BaseImpl and generate an error before checking whether it is deleted.
I wonder now if this behavior is standard compliant.
Edit: I'm attaching the compiler output for the case that one don't want to or can't click on the link to the compiler explorer:
Clang and MSVC are correct, and gcc has a bug.
Defining a function as deleted is not the same as not declaring the function.
Except for move constructors, move assignment functions, and some cases of inherited constructors (see [over.match.funcs]/8), a deleted function is considered to exist for purposes of overload resolution. Nothing else in section [over] treats a deleted function specially. And we have [over.best.ics]/2, emphasis mine:
Implicit conversion sequences are concerned only with the type, cv-qualification, and value category of the argument and how these are converted to match the corresponding properties of the parameter. [ Note: Other properties, such as the lifetime, storage class, alignment, accessibility of the argument, whether the argument is a bit-field, and whether a function is deleted, are ignored. So, although an implicit conversion sequence can be defined for a given argument-parameter pair, the conversion from the argument to the parameter might still be ill-formed in the final analysis. — end note ]
So within impl() : BaseImpl({}) {}, the BaseImpl initializer uses overload resolution to select the BaseImpl constructor used to initialize the base class subobject. The candidates are all the constructors of BaseImpl: the intended BaseImpl(const std::vector<std::string>&), the deleted BaseImpl(), the implicitly declared copy constructor BaseImpl(const BaseImpl&), and the implicitly declared (and not deleted) move constructor BaseImpl(BaseImpl&&). At this point, BaseImpl() is not viable since the initializer has one argument. The vector constructor is viable since there is a constructor vector(std::initializer_list<std::string>) which is not explicit and can convert the {} argument to the vector type. The copy and move constructors are also viable since the BaseImpl() constructor is declared, and is not explicit, and "can" convert the {} argument to type BaseImpl. So overload resolution is ambiguous, even though some of the implicit conversion sequences use a deleted function.
When the BaseImpl() = delete; declaration is not present, BaseImpl simply doesn't have any default constructor, since the BaseImpl(const std::vector<std::string>&) declaration prevents implicit declaration of a default constructor. So there is no implicit conversion sequence for {} to BaseImpl, and the copy and move constructors of BaseImpl are not viable for the initialization BaseImpl({}). The vector constructor is the only viable function.
When you declare BaseImpl(BaseImpl&), deleted or not, this is considered a copy constructor (despite missing the usual const), so it prevents the implicit declarations of the copy constructor and move constructor. But this copy constructor is not viable for BaseImpl({}), since the reference to non-const type can't bind to the rvalue temporary BaseImpl object involved in using BaseImpl() (see [over.ics.ref]/3). So only the intended vector constructor is viable.
WIP marker
A comment by #aschepler has thrown the conclusion in doubt. See there for details or to pitch in if you're a language lawyer. I'll have a closer look tonight, then update this answer.
TL;DR
GCC is correct in a quite non-obvious way.
What is happening
The clang error message seems pretty clear:
<source>:28:14: error: call to constructor of 'BaseImpl' is ambiguous
impl() : BaseImpl({}) {}
^ ~~
<source>:14:7: note: candidate constructor (the implicit move constructor)
class BaseImpl : public AbstractBase
^
<source>:14:7: note: candidate constructor (the implicit copy constructor)
<source>:19:5: note: candidate constructor
BaseImpl(const std::vector<std::string>& name_) : name(name_) {}
^
1 error generated.
Compiler returned: 1
There are three constructors that are available for the BaseImpl({}) call -- the generated copy and move constructors, and the vector constructor. Clang does not know which of these to choose.
MSVCs error is a bit less straightforward:
x64 msvc v19.latest (Editor #1)
x64 msvc v19.latest
x64 msvc v19.latest
/std:c++20
123
<Compilation failed>
# For more information see the output window
x64 msvc v19.latest - 2681ms
Output of x64 msvc v19.latest (Compiler #1)
example.cpp
<source>(28): error C2259: 'BaseImpl': cannot instantiate abstract class
<source>(14): note: see declaration of 'BaseImpl'
<source>(28): note: due to following members:
<source>(28): note: 'int AbstractBase::get_number(void)': is abstract
<source>(11): note: see declaration of 'AbstractBase::get_number'
Compiler returned: 2
What happens here is that MSVC attempts to call the copy or move constructor, that for this it attempts to create a temporary of type BaseImpl from the initializer {}, and that this fails because BaseImpl is abstract before it fails because the default constructor is deleted (so you don't get an error message about that).
GCC does not consider the copy and move ctors even if they are explicitly added and so just constructs a vector and compiles fine.
What should happen
Let's dive into the standard. In particular, let's have a look at [dcl.init.general]. I'll omit non-matching parts of the standard language and denote that with (...).
First note that according to [dcl.init.general] (15)
15 The initialization that occurs
(15.1) — for an initializer that is a parenthesized expression-list or a braced-init-list,
(...)
is called direct-initialization.
There follows in [dcl.init.general] (16) a long list of conditions for what happens in initialization. The relevant here is (16.6)
(16.6) — Otherwise, if the destination type is a (possibly cv-qualified) class type:
(...)
(16.6.2) — Otherwise, if the initialization is direct-initialization, or if it is copy-initialization where the cv-unqualified version of the source type is the same class as, or a derived class of, the class of the destination, constructors are considered. The applicable constructors are enumerated (12.4.2.4), and the best one is chosen through overload resolution (12.4). Then:
(16.6.2.1) — If overload resolution is successful, the selected constructor is called to initialize the object, with the initializer expression or expression-list as its argument(s).
(...)
(16.6.2.3) — Otherwise, the initialization is ill-formed.
What this boils down to is: we look for all applicable constructors and attempt to choose a correct one. If that works, we use it, otherwise it's an error.
So let's take a look at overload resolution in [over.match.ctor]. Here it states that
1 When objects of class type are direct-initialized (9.4), (...), overload resolution selects the constructor. For direct-initialization or default-initialization that is not in the context of copy-initialization, the candidate functions are all the constructors of the class of the object being initialized. (...). The argument list is the expression-list or assignment-expression of the initializer.
So our set of candidate functions are the generated copy and move ctors as well as the vector ctor. Next step is checking which of these are viable according to [over.match.viable]. This means first checking that the number of arguments in the call fits the candidate functions (true for all candidates) and then that
4 Third, for F to be a viable function, there shall exist for each argument an implicit conversion sequence (12.4.4.2) that converts that argument to the corresponding parameter of F. If the parameter has reference type, the implicit conversion sequence includes the operation of binding the reference, and the fact that an lvalue reference to non-const cannot be bound to an rvalue and that an rvalue reference cannot be bound to an lvalue can affect the viability of the function (see 12.4.4.2.5).
An implicit conversion sequence is, according to [over.best.ics.general],
3 A well-formed implicit conversion sequence is one of the following forms: >
(3.1) — a standard conversion sequence (12.4.4.2.2),
(3.2) — a user-defined conversion sequence (12.4.4.2.3), or
(3.3) — an ellipsis conversion sequence (12.4.4.2.4).
where a standard conversion sequence is chiefly concerned with stuff like int to long, lvalue to rvalue, ref to const ref etc. We're interested in user-defined conversion sequences here, which are
1 A user-defined conversion sequence consists of an initial standard conversion sequence followed by a user- defined conversion (11.4.8) followed by a second standard conversion sequence. If the user-defined conversion is specified by a constructor (11.4.8.2), the initial standard conversion sequence converts the source type to the type of the first parameter of that constructor. (...)
2 The second standard conversion sequence converts the result of the user-defined conversion to the target type for the sequence; any reference binding is included in the second standard conversion sequence. (...).
(...)
There is quite definitely a user-defined conversion sequence from {} to std::vector<std::string>. Because BaseImpl's default constructor is deleted, there is not a user-defined conversion sequence from {} to BaseImpl; this would require two user-defined conversions: one to std::vector<std::string> and another to BaseImpl.
So of the three candidate constructors, only the std::vector<std::string> constructor is viable and eligible for overload resolution and should be chosen. GCC does this, and unless I made an error in my analysis, MSVC and clang have a bug.
Consider the following two classes:
class B
{
public:
B() { }
B(const B& b) = delete; //Move ctor not implicitly declared
};
class A
{
public:
A() { }
operator B()
{
return B();
}
};
I can see why this code compiles fine:
A a;
B b = a;
Following the rules of copy-initialization, the object "a" gets converted to a prvalue of type B and since in C++17 the copy constructor is not needed anymore there's no error:
If T is a class type, and the cv-unqualified version of the type of
other is not T or derived from T, or if T is non-class type, but the
type of other is a class type, user-defined conversion sequences that
can convert from the type of other to T (or to a type derived from T
if T is a class type and a conversion function is available) are
examined and the best one is selected through overload resolution. The
result of the conversion, which is a prvalue temporary (until
C++17)prvalue expression (since C++17) if a converting constructor was
used, is then used to direct-initialize the object. The last step is
usually optimized out and the result of the conversion is constructed
directly in the memory allocated for the target object, but the
appropriate constructor (move or copy) is required to be accessible
even though it's not used. (until C++17)
However why does this direct list-initialization compile too?
A a;
B b{ a };
I couldn't find any wording in the list-initialization stating the compiler should attempt to convert A into B in this case. Only that overload resolution on constructors is considered:
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
However in this case the copy constructor is deleted, so shouldn't it not be selected by overload resolution?
This is CWG 2327. You're correct as far as the standard goes, but some compilers additionally consider conversion functions in this context as well - because it really makes sense to.
Consider simple statement (Taken from Is there a difference in C++ between copy initialization and direct initialization?):
A c2 = A();
This statement value-initializes a temporary and then copies that
value into c2 (Read 5.2.3/2 and 8.5/14). This of course will require a
non-explicit copy constructor (Read 8.5/14 and 12.3.1/3 and
13.3.1.3/1)
[Mind the bold sentence in above para] -> My question is why?
Now consider this code :
class B {};
struct A
{
A(B const&) {}
A(A const&) = delete;
//A(A const&); //delete above statement and uncomment this statement,
//and everything works, even though there in no body of copy constructor Oo
};
A a2 = B(); //error since there is no copy constructor oO
Why copy-initialization requires presence of copy constructor even though it's not needed sometime as presented in above code
Please please one more thing :
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.
[Mind the bolding in the following para]
Doesn't that means direct initialization have access to all constructors and can perform implicit conversion sequence , while copy initialization all can do is perform implicit conversion sequence? . What I mean to ask is , implicit conversion in direct initialization is different from implicit conversion sequence in copy initialization ?
The rules for evaluating
A a1 = B(); // (1) copy-initialization
A a2 = {B()}; // (2) copy-initialization
A a3{B()}; // (3) direct-initialization
come from [dcl.init]/17:
— If the initializer is a (non-parenthesized) braced-init-list, the object or reference is list-initialized (8.5.4).
— [...]
— If the destination type is a (possibly cv-qualified) class type:
If the initialization is direct-initialization, or if it is copy-initialization where the cv-unqualified
version of the source type is the same class as, or a derived class of, the class of the destination,
constructors are considered. [...]
Otherwise (i.e., for the remaining copy-initialization cases), user-defined conversion sequences
that can convert from the source type to the destination type or (when a conversion function
is used) to a derived class thereof are enumerated as described in 13.3.1.4, and the best one is
chosen through overload resolution (13.3). [...] The result of the call (which is the temporary for the constructor case) is then used to direct-initialize, according to the rules above,
the object that is the destination of the copy-initialization. In certain cases, an implementation
is permitted to eliminate the copying inherent in this direct-initialization by constructing the
intermediate result directly into the object being initialized; see 12.2, 12.8.
For both a2 and a3, the initializer is a braced-init-list, so we just do list-initialization. This ends up calling the B const& constructor.
For a1, the first sub-bullet doesn't apply - since the source type (B) is not the same or derived class of the destination type (A). So we go into the second sub-bullet point which involves considering conversion functions. There is one (A(B const&)) so we effectively rewrite the expression
A a1_new{A{B{}}};
Now typically, this extra copy will be elided. But you're explicitly prohibiting it, so the code cannot compile.
As to why the differentiation? I don't know. It seems like copy-initialization should simply be syntactic sugar for direct-initialization. In most cases, after all, it is...
(The following applies to C++11)To avoid rambling too much, A a2 = B(); has two stage : first, A temp object of type B is created when you write B(). Then, your function A(B const&) {} will not be invoked here with the role of directly initializing A because you use the copy-initialization syntax. If you want to invoke it to directly initialize A, you should write A a2(B()) instead, rather than using the copy-initialization syntax(the devil here is =). So, what's next? You get a temp object of type B, and you want to use it to initialize obj of type A, and now, you indirectly initialize A by converting B() to the type A. So your function A(B const&) {} is used as a type conversion rather than directly initializing A.
Because of conversion, a temp obj of type A is created, and then we need copy constructor to initialize a2 using that temporary obj.
In copy initialization, type-conversion-functions with explicit keyword can't be called. The design philosophy of explicit keyword is that you should directly invoke it. Combined with type-conversion-functions, those explicit attributed functions should not be used for implicit type conversion. In direct initialization, they can all be called even they are explicit.
To make things interesting, if you make your copy constructor and move constructor explicit, you can't even write T obj = T{}, but you can write T obj {T{}}. You can view copy and move ctor as a conversion too, only the destination and source type are of the same class.
I would like to provide you with further readings here. After that, read this question to know about copy-list-initialization and this question to learn about aggregate-types.
In bullet point (17.6.2) in §8.5[dcl.init]/17 in N4140 we have (emphasis is mine):
Otherwise (i.e., for the remaining copy-initialization cases),
user-defined conversion sequences that can convert from the source
type to the destination type or (when a conversion function is used)
to a derived class thereof are enumerated as described in 13.3.1.4,
and the best one is chosen through overload resolution (13.3). If the
conversion cannot be done or is ambiguous, the initialization is
ill-formed. The function selected is called with the initializer
expression as its argument; if the function is a constructor, the call
initializes a temporary of the cv-unqualified version of the
destination type. The temporary is a prvalue. The result of the call
(which is the temporary for the constructor case) is then used to
direct-initialize, according to the rules above, the object that is
the destination of the copy-initialization. In certain cases, an
implementation is permitted to eliminate the copying inherent in this
direct-initialization by constructing the intermediate result directly
into the object being initialized; see 12.2, 12.8.
The portion of the text in bold seems to indicate that direct-initializations will never invoke a user-defined conversion sequence. But that's not what I found out below:
#include <iostream>
struct A {
A() { std::cout << "default ctor A" << '\n'; }
A(const A&) { std::cout << "copy A" << '\n'; }
};
struct C {
C() { std::cout << "default ctor C" << '\n'; }
operator A() { std::cout << "C::operator A()" << '\n'; return A(); };
};
int main()
{
C c;
A a{c}; // direct-initialization where the C::operator A() is invoked
// to copy construct the object `a`.
}
The following is printed by this snippet:
default ctor C
C::operator A()
default ctor A
copy A
copy A
See live example
Edit
In response to #Johannes answer, please consider A a(c); instead of A a{c};. The rest remains valid, as far as I can understand.
When discussing direct initialization (emphasis is my own):
If the initialization is direct-initialization, or if it is copy-initialization where the cv-unqualified
version of the source type is the same class as, or a derived class of, the class of the destination,
constructors are considered. The applicable constructors are enumerated (13.3.1.3), and the best
one is chosen through overload resolution (13.3). The constructor so selected is called to initialize
the object, with the initializer expression or expression-list as its argument(s). If no constructor
applies, or the overload resolution is ambiguous, the initialization is ill-formed.
This line leads to section §8.5 (2.8,2.9) on Overload Resolutions which describes the process.
Overload resolution selects the function to call in seven distinct contexts within the language:
...
(2.4) — invocation of a constructor for direct-initialization (8.5) of a class object...
But, once the candidate functions and argument lists have been identified, the selection of the best function
is the same in all cases:
(2.8) — First, a subset of the candidate functions (those that have the proper number of arguments and meet
certain other conditions) is selected to form a set of viable functions (13.3.2).
(2.9) — Then the best viable function is selected based on the implicit conversion sequences (13.3.3.1) needed
to match each argument to the corresponding parameter of each viable function.
Given the above, direct initialization does in fact go through the process of selecting candidate constructors (in your case, the one parameter constructor A::A( A const &) ) and finding an implicit converse sequence to match the two together - that is operator C::A().
It is somewhat misleading that they apply - what seems to be - redundant language in the text for other copy-initialization cases.
The extra language on an implicit conversion sequence - that you quote in the question - for the other copy-initialization cases of (17.6.2) directly refer to the following case:
class B {
operator A();
};
class A {
};
B b;
A a = b;
This isn't covered under the first point in (17.6.1) as B doesn't derive from A nor do any helpful constructors exist. Therefore it falls under the 'other' clause.
I don't understand your concern, but answering what happens, in case this can solve it for you. A a{c} directly branches off of bullet 1 of dcl.initp17 to dcl.init.list.
From there it happens that it will initialize a const A& from a C (by over.match.list selecting the copy-constructor of A). This will invoke the paragraph you cited (by first branching off to dcl.init.ref and then applying dcl.initp17 in turn). This will construct a normal user-defined conversion sequence which is able to use conversion functions, such as your operator A.
I think you are confused by the fact that it only mentions user defined conversion sequences for the copy initializations, but in the preceeding bullet it doesn't. The reason is that when doing direct initialization, that is conceptual a direct call to the constructors of a class instead of being a conversion (in which the call to constructors is only a part, surrounded by a standard conversion sequence before (for constructors) and after (for conversion functions/operators) it). In other words, for direct initialization, you already force the constructor call, rather than making the compiler find out that it needs to do it. And you haven't "paid" a user defined conversion sequence (you cannot nest user defined conversion sequences, so they are "precious") - the compiler can still apply a user defined conversion sequence for matching up the argument for the constructor.
The proper testcase for this would be A a(c) rather than A a{c} though, to prevent branching off to dcl.init.list.
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...