43

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?

Casey
  • 41,449
  • 7
  • 95
  • 125
R. Martinho Fernandes
  • 228,013
  • 71
  • 433
  • 510
  • 2
    I'm not sure but I think it's quite logic. Calling f({ "answer", 42 }), you may never know you're passing a foo, and the constructor you try to use is explicit which enforces explicit conversion. – Jaffa Feb 06 '12 at 07:55
  • 1
    @Geoffroy: if something else can be passed from `{ "answer", 42 }`, overload resolution will be ambiguous and thus force me to make the type explicit. – R. Martinho Fernandes Feb 06 '12 at 07:58
  • `by using list initialization syntax the programmer is already expressing the desire to do some kind of conversion`: but not to foo. What if `f()` has another overload that accepts initializer lists? – sehe Feb 06 '12 at 07:59
  • 2
    I don't understand why you don't consider that conversion implicit. – Mat Feb 06 '12 at 08:01
  • 1
    sehe: "What if f() has another overload that accepts initializer lists?" What if it did? {"answer", 42} is not an initializer list, since the elements do not have the same type. Therefore it cannot select a function taking an initializer list. – Nicol Bolas Feb 06 '12 at 08:38
  • @Mat: because there's no conversion. – R. Martinho Fernandes Feb 06 '12 at 09:31
  • 2
    Ok, but you'd still be implicitly creating a `foo ` in `f({"a",1});` whatever the exact sequence of steps are involved. Haven't you explicitly requested that that _not_ happen with `explicit`? – Mat Feb 06 '12 at 09:58

4 Answers4

29

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

Marco A.
  • 43,032
  • 26
  • 132
  • 246
Johannes Schaub - litb
  • 496,577
  • 130
  • 894
  • 1,212
  • 1
    Awesome! Finally *an actual example* of something that goes wrong. Sadly, this means this can't be easily fixed :( (I suppose you mean `void f(int i)` in the last example, right?) – R. Martinho Fernandes Feb 06 '12 at 10:32
  • @RMartin no i meant "Int". If it was "int" then it would be an exact match and would be chosen over the other user defined conversion. The "Int" is meant to be a simple wrapper, like the famous SafeInt class. – Johannes Schaub - litb Feb 06 '12 at 10:38
  • Ah, I get it now. There is only only constructor that is actually truly applicable: that of `Int`. But since the `explicit` one from `String` is considered anyway, the call is ambiguous. Thanks. – R. Martinho Fernandes Feb 06 '12 at 10:41
  • Maybe add a very brief one-line definition of `Int` to make the last example clearer. Like Martinho I only got it after reading the comments (it’s not ambiguous but it’s not totally clear if you don’t expect it). – Konrad Rudolph Feb 06 '12 at 13:29
  • @konrad thanks, perhaps it is better to replace it by std::vector anyway – Johannes Schaub - litb Feb 06 '12 at 14:37
2

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?

Tom Tanner
  • 9,244
  • 3
  • 33
  • 61
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.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
1

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
Serge Dundich
  • 4,221
  • 2
  • 21
  • 16
  • The code doesn't involve conversions (except for the `std::string`). – R. Martinho Fernandes Feb 06 '12 at 09:30
  • 1
    @R. Martinho Fernandes: Well. OK. I'll use the word _cast_ if it is any better. IMO it is pretty much analogous to initialization of a variable of one type with a value of another type which is exactly what implicit conversion is (and it is using a constructor directly at the same time). – Serge Dundich Feb 06 '12 at 10:43
  • It's not a cast either. The difference between this and an implicit conversion is that an implicit conversion happens without you writing *any* code for it. For this you write code to initialize an object (`{ }`). When you look at the code it's clear that something is happening. – R. Martinho Fernandes Feb 06 '12 at 10:47
  • 2
    @R. Martinho Fernandes: "When you look at the code it's clear that something is happening." Yes. Something _implicit_ is happening. **explicit** keyword means that you have to use the constructor only _explicitly_. In such case you wouldn't just see that _something_ is happening - you would see what exactly is happening (i.e. that exactly this particular constrictor is used). – Serge Dundich Feb 06 '12 at 11:05