4
votes
#include <vector>
using namespace std;

class A
{
public:
    explicit A(const initializer_list<int> & a) {}
};
void func(const vector<int>& a)
{

}
void func(A a)
{

}
int main(void)
{
    func({ 1,2,3 });
}

This code fails to compile:

(19): error C2668: 'func': ambiguous call to overloaded function

(13): note: could be 'void func(A)'

(9): note: or 'void func(const std::vector> &)' with[_Ty=int] (19): note: while trying to match the argument list '(initializer list)'

Note that I specified 'explicit' on A's constructor.

In my view, func(A a) should not be considered as a candidate of {1,2,3}. And actually, it is not. If I remove func(const vector<int>& a), then the code still fails, instead of succeeding by removing ambiguity.

In summary, in this code, the func(const vector<int>& a) is the only callable function for {1,2,3}, so there is no ambiguity.

My question is..

  1. How does C++ overloading resolution procedures come to conclusion of 'ambiguous'?

  2. Why doesn't C++ just simply choose callable one?

3
This compiles fine with clang: godbolt.org/z/tr8Otk - Holt
@Holt Breaks on GCC: godbolt.org/z/UMyaKj - NathanOliver
Oh.. I see. But my Visual Studio 2017 says 'failed' with C2668 - i.stav
Please copy the entire text of the error from the Output Tab. And I mean Output tab. The errors list is has an abbreviated format and is not as suitable for copy / paste. - drescherjm
Here's a full error message. (21): error C2668: 'func': ambiguous call to overloaded function (15): note: could be 'void func(A)' (11): note: or 'void func(const std::vector<int,std::allocator<_Ty>> &)' 1> with 1> [ 1> _Ty=int 1> ] (21): note: while trying to match the argument list '(initializer list)' - i.stav

3 Answers

1
votes

explicit constructors are not ignored when you perform list initialization. Such constructors are always considered as viable overload candidates. What happens is, if the system attempts to call an explicit constructor under copy-list-initialization (ie: after overload resolution), then you get a hard compile error.

In your case, it never gets that far because the overload set is ambiguous.

explicit doesn't mean "doesn't exist if you try to convert"; it means "error if you try to convert". The point of explicit is to force the user to think about what type they actually want to use. It's there to prevent a user from writing code that is somewhat ambiguous to the reader.

1
votes

I believe clang is correct here. Overload resolution in C++ works in three phases: First a set of candidate functions is constructed, which is the set of all functions that the call may potentially refer to (basically, the set of all functions that name resolution picks up). This initial set of candidate functions is then narrowed down to arrive at a set of viable functions (the set of functions that could take a call with the given arguments). Finally, the viable functions are ranked to determine the best viable function. This best viable function is what ultimately will be called.

From [over.match.viable]/4

Third, for F to be a viable function, there shall exist for each argument an implicit conversion sequence that converts that argument to the corresponding parameter of F. […]

Based on [over.best.ics]/6, particularly

When the parameter type is not a reference, the implicit conversion sequence models a copy-initialization of the parameter from the argument expression. […]

it would seem that there is no such implicit conversion sequence for void func(A a) due to the necessary constructor being marked explicit (copy-initialization would fail). Therefore, the function is not a viable function and is not considered anymore for overload resolution, which leaves void func(const vector<int>& a) as the only viable candidate, which is the function that will then be called.

Also, purely on a conceptional level, it would seem to make sense that the copy-list-initialization of a parameter can only be ill-formed once we actually know which parameter we're going to initialize, i.e., know which function is actually going to be called. If a call to an overload set would be illegal the moment there is a single argument that is not a valid initializer for the corresponding parameter in every single potential candidate function, then what's the point of overloading? As long as we're still working on figuring out which function to call, there is no way to decide whether the initialization would be ill-formed or not. clang exhibits exactly this behavior. When you comment out the void func(const std::vector<int>& a) overload, clang will suddenly complain that the call is ill-formed…

try it out here

0
votes

I agree with @Nicol Bolas. MSVC and gcc are right while clang and icc are wrong.

In overloading resolution, list initialization differs from copy initialization that list initialization considers explicit constructors while copy initialization doesn't.

(From cppreference)

List-initialization When an object of non-aggregate class type T is list-initialized, two-phase overload resolution takes place.

at phase 1, the candidate functions are all initializer-list constructors of T and the argument list for the purpose of overload resolution consists of a single initializer list argument if overload resolution fails at phase 1, phase 2 is entered, where the candidate functions are all constructors of T and the argument list for the purpose of overload resolution consists of the individual elements of the initializer list. If the initializer list is empty and T has a default constructor, phase 1 is skipped.

In copy-list-initialization, if phase 2 selects an explicit constructor, the initialization is ill-formed (as opposed to all over copy-initializations where explicit constructors are not even considered).

Some examples:

This one

#include <iostream>
#include <initializer_list>

struct A
{
    explicit A(int, int, int) {}
};

struct B
{
    B(std::initializer_list<int>) {}
};

void f(A)   //f1
{
    std::cout << 1 << std::endl;
}

void f(B)   //f2
{
    std::cout << 2 << std::endl;
}

int main()
{
    f({ 1,2,3 });  //list initialziation
}

fails on MSVC and gcc. (See here and here)

This one

#include <iostream>
#include <initializer_list>

struct A
{
    explicit A(std::initializer_list<int>) {}
};

struct B
{
    B(std::initializer_list<int>) {}
};

void f(A)   //f1
{
    std::cout << 1 << std::endl;
}

void f(B)   //f2
{
    std::cout << 2 << std::endl;
}

int main()
{
    f({ 1,2,3 });  //Also list initialization
}

also fails on MSVC and gcc. (See here and here)

While this one

#include <iostream>
#include <initializer_list>

struct A
{
    explicit A(int) {}
};

struct B
{
    B(int) {}
};

void f(A)   //f1
{
    std::cout << 1 << std::endl;
}

void f(B)   //f2
{
    std::cout << 2 << std::endl;
}

int main()
{
    f(1);  //Copy initialization
}

successes on all four compilers.