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

2

6

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?

Uninstructed answered 1/10, 2012 at 16:59 Comment(7)
list initialization is not supposed to be able to replace all uses of direct initialization and copy initialization. We need to find whoever is spreading this misinformation. :/Myeloid
The more confusing thing is that by the same passage, X x2 = Y{}; should be illegal too, since that falls under the 13.3.1.3 part ("when invoked for the copying/moving of the temporary in the second step of a class copy-initialization"). It likely that there's something more going on here.Hebraism
@NicolBolas I'd think the first step in class copy-initialization would be to use the conversion operator to obtain a (temporary) X, and the second step to copy from that X. Or at least this is how I read it...Heald
@MooingDuck Still, this particular difference between the two initialization methods seems rather, um, strange. Especially because it works if you pass a second Y (and modify the constructor accordingly, of course). My answer below contains more details.Heald
@MooingDuck Stroustrup's uniform initializer ideal is (or was) "a single syntax that can be used everywhere and wherever it’s used, the same initialized value results for the same initializer value" - doesn't that argue for replacing other initialization syntaxes?Uninstructed
(1) I can't find that quote in that link. (2) Even if that is the ideal, that didn't come to light. To create a vector or integers with a variable N number of elements, you must use the old () constructor syntax. (As others observed, this is all in side note to your question, and not related to answering your question)Myeloid
demonstration here: liveworkspace.org/code/0282cd5c90ff00471dc7355b5602eefb, results at the bottom show std::vector<unsigned> a{5} has a size of 1, and std::vector<std::string> b{5} has a size of 5.Myeloid
U
6

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]

Uninstructed answered 4/10, 2012 at 17:40 Comment(3)
I like how you still allow conversion functions. It makes struct A { A(int); }; struct B { operator A(); operator int(); }; B b; A a{ b }; ambiguous instead of choosing the A(int) ctor (like it currently does). The current behavior seems surprising.Palladous
@JohannesSchaub-litb I have been trying to find where the standard says that: "more than one user-defined conversion is allowed in an implicit conversion sequence in the context of list-initialization", however I cannot find it. All I could find was something similar to: "user conversions are allowed to convert the elements of the initializer list to the types of the parameters of the constructors". Could you please tell me where it is said that multiple conversions are allowed? Thank you.Darladarlan
@Uninstructed I am trying to understand why in the example that you gave (second code block) the copy constructor would be an option if the wording in 13.3.3.1/4 didn't forbid it. I opened a new question about it here: #51620455. I would greatly appreciate it if you could help me understand what are the steps involved. Thank you.Darladarlan
H
3

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

Heald answered 1/10, 2012 at 19:8 Comment(2)
Sounds like a defect in the spec. Best to report it as such. There's no reason why X{Y{}, Y{}} should work when X{Y{}} won't.Hebraism
@NicolBolas I've drafted a defect report below: https://mcmap.net/q/1012134/-is-it-possible-to-invoke-a-user-defined-conversion-function-via-list-initializationUninstructed

© 2022 - 2024 — McMap. All rights reserved.