c++c++11language-lawyeruniform-initializationlist-initialization

How does the number of braces affect uniform initialization?


Consider the following code snippet:

#include <iostream>

struct A {
  A() {}
  A(const A&) {}
};

struct B {
  B(const A&) {}
};

void f(const A&) { std::cout << "A" << std::endl; }
void f(const B&) { std::cout << "B" << std::endl; }

int main() {
  A a;
  f(   {a}   ); // A
  f(  {{a}}  ); // ambiguous
  f( {{{a}}} ); // B
  f({{{{a}}}}); // no matching function
}

Why does each call fabricate the corresponding output? How does the number of braces affect uniform initialization? And how does brace elision affect all this?


Solution

  • Overload resolution is fun like this.

    1. {a} has exact match rank for initializing (a temporary for) the const A& parameter, which outcompetes the user-defined conversion B(const A&) as a realization of {a}. This rule was added in C++14 to resolve ambiguities in list-initialization (along with adjustments for aggregates).

      Note that the notional temporary is never created: after overload resolution picks f(const A&), the reference is simply initialized to refer to a, and this interpretation can apply even for non-copyable types.

    2. It would be permissible to initialize a const A& parameter (as above) to the constructor for either A or B, so the call is ambiguous.
    3. Calling a copy constructor (here, A(const A&)) repeatedly is prohibited as multiple user-defined conversions—rather than allowing one such conversion per level of overload resolution. So the outermost braces must initialize a B from the A initialized from {{a}} as (permitted) in the second case. (The middle layer of braces could initialize a B, but copying it with the outer layer would be prohibited and there’s nothing else to try to initialize.)
    4. Every interpretation involves such a disallowed extra conversion.

    No brace elision is involved—we don’t know the outermost target type to allow it.