What could go wrong if copy-list-initialization al

2019-01-13 20:16发布

问题:

In the C++ standard, §13.3.1.7 [over.match.list], the following is stated:

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

This is the reason why we can't do, for example, something like this:

struct foo {
    // explicit because it can be called with one argument
    explicit foo(std::string s, int x = 0);
private:
    // ...
};

void f(foo x);

f({ "answer", 42 });

(Note that what happens here is not a conversion, and it would not be one even if the constructor was "implicit". This is initialization of a foo object using its constructor directly. Other than the std::string, there is no conversion here.)

This seems perfectly fine to me. There's no way that an implicit conversion will bite me.

If { "answer", 42 } can initialize something else, the compiler won't betray me and do the wrong thing:

struct bar {
    // explicit because it can be called with one argument
    explicit bar(std::string s, int x = 0);
private:
    // ...
};

void f(foo x);
void f(bar x);

f({ "answer", 42 }); // error: ambiguous call

There's no problem: the call is ambiguous, the code won't compile, and I'll have to pick the overload explicitly.

f(bar { "answer", 42 }); // ok

Since the prohibition is explicitly stated, I have the feeling that I am missing something here. As far as I can see, list initialization picking explicit constructors doesn't seem like a problem to me: by using list initialization syntax the programmer is already expressing the desire to do some kind of "conversion".

What could go wrong? What am I missing?

回答1:

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



回答2:

This statement:

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

means many things. Among them, it means that it must look at explicit constructors. After all, it can't select an explicit constructor if it can't look at it. When it looks for candidates to convert the braced list into, it must select from all candidates. Even the ones that will later be found to be illegal.

If overload resolution results in multiple functions being equally viable, then it results in an ambiguous call that requires manual user intervention.



回答3:

As I understand the very purpose of the keyword explicit is denying implicit cast with this constructor.

So you are asking why explicit constructor cannot be used for implicit cast? Obviously because the author of that constructor explicitly denied it by using keyword explicit with it. The quote from the standard you've posted just states that explicit keyword applies also to initializer-lists (not just to simple values of some type).

ADD:

To say more correctly: the purpose of the keyword explicit used with some constructor is making it absolutely clear that this constructor is used in some place (i.e. forcing all the code to invoke this constructor explicitly).

And IMO statement like f({a,b}) when f is a name of the function has nothing to do with explicit constructor call. It is absolutely unclear (and context dependent) which constructor (and what type) is used here, e.g. it depends on function overloads present.

On the other hand something like f(SomeType(a,b)) is totally different thing - it is absolutely clear that we use the constructor of type SomeType that takes two arguments a,b and that we use the function f overload that will be the best to accept single argument of type SomeType.

So some constructors are OK for implicit use like f({a,b}) and others require that the fact of their using is absolutely clear to the reader that is why we declare them explicit.

ADD2:

My point is: Sometimes it absolutely makes sense to declare constructors explicit even if nothing could go wrong. IMO whether constructor is explicit is more a matter of its logic than caveats of any kind.

E.g.

double x = 2; // looks absolutely natural
std::complex<double> x1 = 3;  // also looks absolutely natural
std::complex<double> x2 = { 5, 1 };  // also looks absolutely natural

But

std::vector< std::set<std::string> >  seq1 = 7; // looks like nonsense
std::string str = some_allocator; // also looks stupid


回答4:

Isn't it because 'explicit' is there to stop implicit casting, and you're asking it to do an implicit cast?

Would you be asking the question if you has specified the structure with a single argument constructor?