Search code examples
c++language-lawyerbraced-init-list

Call of overloaded <brace-enclosed initializer list> is ambiguous with inconvertible types


Related question:


The question is similar to 1 and 2, but not the same.


#include <type_traits>
#include <vector>

struct A {
    A();
};
static_assert(std::is_convertible_v<double, A> == false);
static_assert(std::is_convertible_v<A, double> == false);

void func(std::vector<double> values);
void func(std::vector<A> as);

int main() {
    func({ 4.2 });
}

Apparently, double and A cannot be implicitly converted to each other. So I think void func(std::vector<double>) should be called.

But the results for different compilers are different: https://godbolt.org/z/c1hW47f4c

GCC fails to compile with:

<source>: In function 'int main()':
<source>:14:9: error: call of overloaded 'func(<brace-enclosed initializer list>)' is ambiguous
   14 |     func({ 4.2 });
      |     ~~~~^~~~~~~~~
<source>:10:6: note: candidate: 'void func(std::vector<double>)'
   10 | void func(std::vector<double> values);
      |      ^~~~
<source>:11:6: note: candidate: 'void func(std::vector<A>)'
   11 | void func(std::vector<A> as);
      |      ^~~~
Compiler returned: 1

(VC15 (VS 2017) rejects the example too.)


Which one is right? Why?


Solution

  • tldr;

    The program is ill-formed because during the overload resolution of the call func({4.2}) for the second overload func(std::vector<A> as) the explicit ctor of std::vector that takes a size_t is choosen which isn't allowed in copy-list-initialization.

    Language-Lawyer Explanation

    Step 1

    First let us consider the overload resolution for the call func({4.2}) against the first overload func(std::vector<double>).

    Note note that func({ 4.2 }) is copy-initialization:

    The initialization that occurs in the = form of a brace-or-equal-initializer or condition ([stmt.select]), as well as in argument passing, function return, throwing an exception ([except.throw]), handling an exception ([except.handle]), and aggregate member initialization other than by a designated-initializer-clause ([dcl.init.aggr]), is called copy-initialization.

    Now we move onto dcl.init.general#16 to see that this will use list initialization:

    The semantics of initializers are as follows. The destination type is the type of the object or reference being initialized and the source type is the type of the initializer expression. If the initializer is not a single (possibly parenthesized) expression, the source type is not defined.

    • If the initializer is a (non-parenthesized) braced-init-list or is = braced-init-list, the object or reference is list-initialized ([dcl.init.list]).

    So from dcl.init.list we also see that this is copy-list-initialization:

    List-initialization is initialization of an object or reference from a braced-init-list. Such an initializer is called an initializer list, and the comma-separated initializer-clauses of the initializer-list or designated-initializer-clauses of the designated-initializer-list are called the elements of the initializer list. An initializer list may be empty. List-initialization can occur in direct-initialization or copy-initialization contexts; list-initialization in a direct-initialization context is called direct-list-initialization and list-initialization in a copy-initialization context is called copy-list-initialization. Direct-initialization that is not list-initialization is called direct-non-list-initialization.

    Finally we move onto dcl.init.list#3:

    List-initialization of an object or reference of type T is defined as follows:

    • Otherwise, if T is a class type, constructors are considered. The applicable constructors are enumerated and the best one is chosen through overload resolution ([over.match], [over.match.list]). If a narrowing conversion (see below) is required to convert any of the arguments, the program is ill-formed.

    This means that overload resolution is done with std::vector's for the argument {4.2} and the best one will be choosen.

    So we move onto over.match.list:

    When objects of non-aggregate class type T are list-initialized such that [dcl.init.list] specifies that overload resolution is performed according to the rules in this subclause or when forming a list-initialization sequence according to [over.ics.list], overload resolution selects the constructor in two phases:

    • If the initializer list is not empty or T has no default constructor, overload resolution is first performed where the candidate functions are the initializer-list constructors ([dcl.init.list]) of the class T and the argument list consists of the initializer list as a single argument.
    • Otherwise, or if no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all the constructors of the class T and the argument list consists of the elements of the initializer list.

    Note that since an initializer list ctor was found, so overload resolution won't be performed again. This in turn means that the initializer list ctor is the choosen option when matching func({4.2}) against the first overload func(std::vector<double>)

    Step 2

    Now we see how func({4.2}) matches against the second overload func(std::vector<A>).

    In this case, almost all the steps are same(as in the last case) except that this time the initializer list ctor std::vector(std::initializer_list<A>) is not viable and so the statement if no viable initializer-list constructor is found, overload resolution is performed again is satisfied and so

    Otherwise, or if no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all the constructors of the class T and the argument list consists of the elements of the initializer list.

    This means that this time, the std::size_t argument ctor of std::vector will be choosen. But note that this ctor of std::vector is explicit and we have:

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

    Thus the selection of size_t argument ctor of std::vector makes the program ill-formed.