20

Consider this code:

#include <vector>
#include <iostream>

enum class A
{
  X, Y
};

struct Test
{
  Test(const std::vector<double>&, const std::vector<int>& = {}, A = A::X)
  { std::cout << "vector overload" << std::endl; }

  Test(const std::vector<double>&, int, A = A::X)
  { std::cout << "int overload" << std::endl; }
};

int main()
{
  std::vector<double> v;
  Test t1(v);
  Test t2(v, {}, A::X);
}

https://godbolt.org/z/Gc_w8i

This prints:

vector overload
int overload

Why does this not produce a compilation error due to ambiguous overload resolution? If the second constructor is removed, we get vector overload two times. How/by what metric is int an unambiguously better match for {} than std::vector<int>?

The constructor signature can surely be trimmed further, but I just got tricked by an equivalent piece of code and want to make sure nothing important is lost for this question.

Max Langhof
  • 23,383
  • 5
  • 39
  • 72
  • If i recall corretly `{}` as a block of code, assigns 0 to variables - example: const char x = {}; is set to 0 (null char) , same for int etc. – Seti Mar 15 '20 at 19:49
  • 2
    @Seti That is what `{}` effectively does in certain special cases, but it's not generally correct (for starters, `std::vector x = {};` works, `std::vector x = 0;` does not). It's not as simple as "`{}` assigns zero". – Max Langhof Mar 16 '20 at 08:48
  • Right, its not so simple, but it still assigns zero - thought i think that this beaviour is quite confusing and shouldnt be used really – Seti Mar 21 '20 at 10:51
  • 2
    @Seti `struct A { int x = 5; }; A a = {};` does not assign zero in any sense, it constructs an `A` with `a.x = 5`. This is unlike `A a = { 0 };`, which does initialize `a.x` to 0. The zero is not inherent to `{}`, it is inherent to how each type is default-constructed or value-initialized. See [here](http://eel.is/c++draft/dcl.init#17.1), [here](http://eel.is/c++draft/dcl.init#list-3.11) and [here](http://eel.is/c++draft/dcl.init#aggr-5). – Max Langhof Mar 23 '20 at 09:22
  • I still think that default-constructed value are confusing (requires you to check the behavior or keep much knowledge all the time) – Seti Mar 25 '20 at 22:47

1 Answers1

12

It's in [over.ics.list], emphasis mine

6 Otherwise, if the parameter is a non-aggregate class X and overload resolution per [over.match.list] chooses a single best constructor C of X to perform the initialization of an object of type X from the argument initializer list:

  • If C is not an initializer-list constructor and the initializer list has a single element of type cv U, where U is X or a class derived from X, the implicit conversion sequence has Exact Match rank if U is X, or Conversion rank if U is derived from X.

  • Otherwise, the implicit conversion sequence is a user-defined conversion sequence with the second standard conversion sequence an identity conversion.

9 Otherwise, if the parameter type is not a class:

  • [...]

  • if the initializer list has no elements, the implicit conversion sequence is the identity conversion. [ Example:

    void f(int);
    f( { } ); // OK: identity conversion
    

    end example ]

The std::vector is initialized by constructor and the bullet in bold deems it a user defined converison. Meanwhile, for an int, this is the identity conversion, so it trumps the rank of the first c'tor.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
  • Yes, seems accurate. – Columbo Mar 03 '20 at 15:09
  • Interesting to see that this situation is explicitly considered in the standard. I would've really expected it to be ambiguous (and it looks like it could have easily been specified that way). I can't follow the reasoning in your last sentence - `0` has type `int` but not type `std::vector`, how is that an "as if" wrt to the "untyped" nature of `{}`? – Max Langhof Mar 03 '20 at 15:23
  • @MaxLanghof - The other way to look at it is that for non class types, it's not a user defined conversion by any stretch. In stead it's a direct initializer for the default value. Hence an identity in this case. – StoryTeller - Unslander Monica Mar 03 '20 at 15:26
  • That part is clear. I'm surprised by the need for a user-defined conversion to `std::vector`. As you said, I expected "the type of the parameter ends up deciding what's the type of the argument", and a `{}` "of type" (so to speak) `std::vector` should not need (non-identity) conversion to initialize a `std::vector`. The standard obviously says it does, so that's that, but it doesn't make sense to me. (Mind you, I'm not arguing that you or the standard are wrong, just trying to reconcile this with my mental models.) – Max Langhof Mar 03 '20 at 15:29
  • Ok, that edit was not the resolution I hoped for, but fair enough. :D Thanks for your time! – Max Langhof Mar 03 '20 at 15:34
  • @MaxLanghof - Deleted my ramblings because I'm struggling to put it into words clearly. Will revisit later when time permits. – StoryTeller - Unslander Monica Mar 03 '20 at 15:35
  • Looking forward to that :) – Max Langhof Mar 03 '20 at 15:35