Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Strange case of C++11 overload resolution

I came across a rather strange case of overload resolution today. I reduced it to the following:

struct S
{
    S(int, int = 0);
};

class C
{
public:
    template <typename... Args>
    C(S, Args... args);

    C(const C&) = delete;
};

int main()
{
    C c({1, 2});
}

I fully expected C c({1, 2}) to match the first constructor of C, with the number of variadic arguments being zero, and {1, 2} being treated as an initializer list construction of an S object.

However, I get a compiler error that indicates that instead, it matches the deleted copy constructor of C!

test.cpp: In function 'int main()':
test.cpp:17:15: error: use of deleted function 'C(const C &)'
test.cpp:12:5: error: declared here

I can sort of see how that might work - {1, 2} can be construed as a valid initializer for C, with the 1 being an initializer for the S (which is implicitly constructible from an int because the second argument of its constructor has a default value), and the 2 being a variadic argument... but I don't see why that would be a better match, especially seeing as the copy constructor in question is deleted.

Could someone please explain the overload resolution rules that are in play here, and say whether there is a workaround that does not involve mentioning the name of S in the constructor call?

EDIT: Since someone mentioned the snippet compiles with a different compiler, I should clarify that I got the above error with GCC 4.6.1.

EDIT 2: I simplified the snippet even further to get an even more disturbing failure:

struct S
{
    S(int, int = 0);
};

struct C
{
    C(S);
};

int main()
{
    C c({1});
}

Errors:

test.cpp: In function 'int main()':
test.cpp:13:12: error: call of overloaded 'C(<brace-enclosed initializer list>)' is ambiguous
test.cpp:13:12: note: candidates are:
test.cpp:8:5: note: C::C(S)
test.cpp:6:8: note: constexpr C::C(const C&)
test.cpp:6:8: note: constexpr C::C(C&&)

And this time, GCC 4.5.1 gives the same error, too (minus the constexprs and the move constructor which it does not generate implicitly).

I find it very hard to believe that this is what the language designers intended...

like image 591
HighCommander4 Avatar asked Oct 24 '11 04:10

HighCommander4


2 Answers

For C c({1, 2}); you have two constructors that can be used. So overload resolution takes place and looks what function to take

C(S, Args...)
C(const C&)

Args will have been deduced to zero as you figured out. So the compiler compares constructing S against constructing a C temporary out of {1, 2}. Constructing S from {1, 2} is straight forward and takes your declared constructor of S. Constructing C from {1, 2} also is straight forward and takes your constructor template (the copy constructor is not viable because it has only one parameter, but two arguments - 1 and 2 - are passed). These two conversion sequences are not comparable. So the two constructors would be ambiguous, if it weren't for the fact that the first is a template. So GCC will prefer the non-template, selecting the deleted copy constructor and will give you a diagnostic.

Now for your C c({1}); testcase, three constructors can be used

C(S)
C(C const&)
C(C &&)

For the two last, the compiler will prefer the third because it binds an rvalue to an rvalue. But if you consider C(S) against C(C&&) you won't find a winner between the two parameter types because for C(S) you can construct an S from a {1} and for C(C&&) you can initialize a C temporary from a {1} by taking the C(S) constructor (the Standard explicitly forbids user defined conversions for a parameter of a move or copy constructor to be usable for an initialization of a class C object from {...}, since this could result in unwanted ambiguities; this is why the conversion of 1 to C&& is not considered here but only the conversion from 1 to S). But this time, as opposed to your first testcase, neither constructor is a template so you end up with an ambiguity.

This is entirely how things are intended to work. Initialization in C++ is weird so getting everything "intuitive" to everyone is going to be impossible sadly. Even a simple example as above quickly gets complicated. When I wrote this answer and after an hour I looked at it again by accident I noticed I overlooked something and had to fix the answer.

like image 161
Johannes Schaub - litb Avatar answered Oct 22 '22 09:10

Johannes Schaub - litb


You might be correct in your interpretation of why it can create a C from that initializer list. ideone happily compiles your example code, and both compilers can't be correct. Assuming creating the copy is valid, however...

So from the compiler's point of view, it has two choices: Create a new S{1,2} and use the templated constructor, or create a new C{1,2} and use the copy constructor. As a rule, non-template functions are preferred over template ones, so the copy constructor is chosen. Then it looks at whether or not the function can be called... it can't, so it spits out an error.

SFINAE requires a different type of error... they occur during the first step, when checking to see which functions are possible matches. If simply creating the function results in an error, that error is ignored, and the function not considered as a possible overload. After the possible overloads are enumerated, this error suppression is turned off and you're stuck with what you get.

like image 4
Dennis Zickefoose Avatar answered Oct 22 '22 09:10

Dennis Zickefoose