What Could Go Wrong If Copy-List-Initialization Allowed Explicit Constructors

What could go wrong if copy-list-initialization allowed explicit constructors?

Conceptually copy-list-initialization is the conversion of a compound value to a destination type. The paper that proposed wording and explained rationale already considered the term "copy" in "copy list initialization" unfortunate, since it doesn't really convey the actual rationale behind it. But it is kept for compatibility with existing wording. A {10, 20} pair/tuple value should not be able to copy initialize a String(int size, int reserve), because a string is not a pair.

Explicit constructors are considered but forbidden to be used. This makes sense in cases as follows

struct String {
explicit String(int size);
String(char const *value);
};

String s = { 0 };

0 does not convey the value of a string. So this results in an error because both constructors are considered, but an explicit constructor is selected, instead of the 0 being treated as a null pointer constant.

Unfortunately this also happens in overload resolution across functions

void print(String s);
void print(std::vector<int> numbers);

int main() { print({10}); }

This is ill-formed too because of an ambiguity. Some people (including me) before C++11 was released thought that this is unfortunate, but didn't came up with a paper proposing a change regarding this (as far as I am aware).

C++11: in-class initializaton with = {} doesn't work with explicit constructor

I can't explain the rationale behind this, but I can repeat the obvious.

I found this surprising. Is it really the intention of the C++11
standard that this code doesn't compile?

§13.3.1.7

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



Removing the = fixes it: Foo foo { 42 }; but I personally find this
harder to explain to people who have been used to the form with = for
decades, and since the standard refers to a
"brace-or-equal-initializer" it's not obvious why the good old way
doesn't work in this scenario.

Foo foo { 42 } is direct initialization, whereas the equal sign (with braces) makes it copy-list-initialization. Another answer reasons that because compilation fails for copy-initialization (equal sign without braces), then it shouldn't be surprising that it also fails for copy-list-initialization, but the two fail for different reasons.

cppreference:

Direct-initialization is more permissive than copy-initialization:
copy-initialization only considers non-explicit constructors and
user-defined conversion functions, while direct-initialization
considers all constructors and implicit conversion sequences.

And their page on the explicit specifier:

Specifies constructors and (since C++11) conversion
operators that don't allow implicit conversions or
copy-initialization.

On the other hand, for copy-list-initialization:

T object = {arg1, arg2, ...}; (10)

10) on the right-hand-side of the equals sign (similar to copy-initialization)

  • Otherwise, the constructors of T are considered, in two phases:

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

As discussed in What could go wrong if copy-list-initialization allowed explicit constructors?, the compilation fails because the explicit constructor is selected but is not allowed to be used.

Copy list initialisation and explicit constructor allowed?

Yes, VS2013 is wrong in allowing the code to compile.

The important rule is in [over.ics.list] (quote from N3337):

[over.ics.list]/1]: When an argument is an initializer list (8.5.4), it is not an expression and special rules apply for converting
it to a parameter type.

[over.ics.list]/3]: 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. If multiple constructors are viable but
none is better than the others, the implicit conversion sequence is the ambiguous conversion sequence. User-defined conversions are allowed for conversion of the initializer list elements to the constructor parameter
types except as noted in 13.3.3.1
.

13.3.3.1 outlines implicit conversion sequences, which references [class.conv.ctor] regarding user-defined conversions:

[class.conv.ctor]/1: A constructor declared without the function-specifier explicit specifies a conversion from the types of its
parameters to the type of its class. Such a constructor is called a converting constructor.

So the constructor must not be marked explicit if it should be used for this form of initialization.

Why explicit initialization list is more likely to failure?

Here's the example of an explicit initialization list in Lipmann's book.

Point1 local1 = { 1.0, 1.0, 1.0 };

I think the point he's trying to make is that you must remember to use the explicit initialization! In other words they're not a replacement for constructors. If you forget to use the list...

Point local2;

... then you have "failed to initialize the object". It's not that the initialization list can fail in any way, simply that you can fail to remember to use it.

Compare with a constructor

Point::Point (int x=0, int y=0, int z=0) : x(x), y(y) z(z) {};

You can now do both, and still get well defined results.

 Point local3(1.0, 1.0, 1.0);
Point local4; // uses default values of 0,0,0

Is list-initialization an implicit conversion?

Your conclusion that

pair<const type_index, std::string> p = {typeid(int), "int"};

becomes

pair<const type_index, std::string> p(typeid(int), "int");

is not accurate because the first statement is copy-list-initialization while the second is direct-initialization. The two are identical, except that copy-list-initialization is ill-formed if an explicit constructor is chosen (and narrowing conversions are not allowed in the former).

So if the pair constructor in question was defined as

template<class U1, class U2>
explicit constexpr pair(U1&& x, U2&& y);

direct-initialization would still succeed, but copy-list-initialization would fail. Quoting from right below the parts of [over.match.list] you quoted

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


Other than that, everything else you've said is correct. The pair constructor is an implicit conversion because the constructor is not explicit and it's considered for overload resolution according to the second bullet of [over.match.list] because pair doesn't have an initializer list constructor.

Calling an explicit constructor with a braced-init list: ambiguous or not?

As far as I can tell, this is a clang bug.

Copy-list-initialization has a rather unintuitive behaviour: It considers explicit constructors as viable until overload resolution is completely finished, but can then reject the overload result if an explicit constructor is chosen. The wording in a post-N4567 draft, [over.match.list]p1

In copy-list-initialization, if an explicit constructor is chosen, the
initialization is ill-formed. [ Note: This differs from other
situations (13.3.1.3, 13.3.1.4), where only converting constructors
are considered for copy-initialization. This restriction only applies
if this initialization is part of the final result of overload
resolution. — end note ]


clang HEAD accepts the following program:

#include <iostream>
using namespace std;

struct String1 {
explicit String1(const char*) { cout << "String1\n"; }
};
struct String2 {
String2(const char*) { cout << "String2\n"; }
};

void f1(String1) { cout << "f1(String1)\n"; }
void f2(String2) { cout << "f2(String2)\n"; }
void f(String1) { cout << "f(String1)\n"; }
void f(String2) { cout << "f(String2)\n"; }

int main()
{
//f1( {"asdf"} );
f2( {"asdf"} );
f( {"asdf"} );
}

Which is, except for commenting out the call to f1, straight from Bjarne Stroustrup's N2532 - Uniform initialization, Chapter 4. Thanks to Johannes Schaub for showing me this paper on std-discussion.

The same chapter contains the following explanation:

The real advantage of explicit is that it renders f1("asdf") an
error. A problem is that overload resolution “prefers” non-explicit
constructors, so that f("asdf") calls f(String2). I consider the
resolution of f("asdf") less than ideal because the writer of
String2 probably didn’t mean to resolve ambiguities in favor of
String2 (at least not in every case where explicit and non-explicit
constructors occur like this) and the writer of String1 certainly
didn’t. The rule favors “sloppy programmers” who don’t use explicit.


For all I know, N2640 - Initializer Lists — Alternative Mechanism and Rationale is the last paper that includes rationale for this kind of overload resolution; it successor N2672 was voted into the C++11 draft.

From its chapter "The Meaning Of Explicit":

A first approach to make the example ill-formed is to require that all
constructors (explicit and non-explicit) are considered for implicit
conversions, but if an explicit constructor ends up being selected,
that program is ill-formed. This rule may introduce its own surprises;
for example:

struct Matrix {
explicit Matrix(int n, int n);
};
Matrix transpose(Matrix);

struct Pixel {
Pixel(int row, int col);
};
Pixel transpose(Pixel);

Pixel p = transpose({x, y}); // Error.

A second approach is to ignore the explicit constructors when looking
for the viability of an implicit conversion, but to include them when
actually selecting the converting constructor: If an explicit
constructor ends up being selected, the program is ill-formed. This
alternative approach allows the last (Pixel-vs-Matrix) example to work
as expected (transpose(Pixel) is selected), while making the
original example ("X x4 = { 10 };") ill-formed.

While this paper proposes to use the second approach, its wording seems to be flawed - in my interpretation of the wording, it doesn't produce the behaviour outlined in the rationale part of the paper. The wording is revised in N2672 to use the first approach, but I couldn't find any discussion about why this was changed.


There is of course slightly more wording involved in initializing a variable as in the OP, but considering the difference in behaviour between clang and gcc is the same for the first sample program in my answer, I think this covers the main points.

Why does C++ list initialization also take regular constructors into account?

Essentially it is a mess up. For C++11 it was attempted to create one uniform way to initialize objects instead of multiple approaches otherwise necessary:

  • T v(args...); for the usual case
  • T d = T(); when using the default constructor for stack-based objects
  • T m((iterator(x)), iterator()); to fight the Most Vexing Parse (note the extra parenthesis around the first parameter)
  • T a = { /* some structured values */ }; for aggregate initialization

Instead the Uniform Initialization Syntax was invented:

T u{ /* whatever */ };

The intention was that uniform initialization syntax would be used everywhere and the old stule would go out of fashion. Everything was fine except that proponents of initialization from std::initializer_list<S> realized that the syntax would be something like that:

std::vector<int> vt({ 1, 2, 3 });
std::vector<int> vu{{ 1, 2, 3 }};

That was considered unacceptable and uniform initialization syntax was irreparably compromised to allow the so much better

std::vector<int> vx{ 1, 2, 3 };

The problem with this mixture is that it now is sometimes unclear what is actually meant and uniform initialization syntax isn’t uniform any more. It is still necessary in some contexts (especially to value initialize stack-based objects in generic code) but it isn’t the correct choice in all cases. For example, the following two notations were meant to mean the same thing but they don’t:

std::vector<int> v0(1, 2); // one element with value 2
std::vector<int> v1{1, 2}; // two elements: 1 and 2

tl;dr: initializer list and uniform initialization syntax are the two separate notations. Sadly, they conflict.



Related Topics



Leave a reply



Submit