9

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...

HighCommander4
  • 50,428
  • 24
  • 122
  • 194
  • 6
    The compiler doesn't consider "can I call this?" until after it has decided "what should I call." So the copy constructor being deleted has no bearing on the subject at all, unfortunately.. – Dennis Zickefoose Oct 24 '11 at 04:04
  • I don't understand why it would call the copy constructor here at all - wouldn't that imply that it constructs a temporary `C`, then immediately copies that into a new local variable instance of `C`?? Curiouser and curiouser... – bdonlan Oct 24 '11 at 04:07
  • 1
    @Dennis: isn't SFINAE a counterexample to that? – HighCommander4 Oct 24 '11 at 04:10
  • 1
    @bdonlan: Think about it this way: If I had written `C c(C{1, 2})` it would have no choice but to call the copy constructor. If I had written `C c(S{1, 2})` it would have no choice but to call the first constructor. But I wrote `C c({1, 2})`, so which will it call? On a common-sense level it's a no-brainer that it should not try to call a deleted function... but then compilers were never strong in common sense, were they? =P – HighCommander4 Oct 24 '11 at 04:13
  • 1
    I feel like there's some sort of parenthesizing/bracing that would solve this problem. – Michael Price Oct 24 '11 at 04:22
  • 1
    @HighCommander4: Calling and template instantiating are different things - in this case no template instantiation is even attempted – Lightness Races in Orbit Oct 24 '11 at 04:23
  • @HighCommander4 I think it makes just as little sense to prefer object construction from a different class when you have a perfectly fine constructor that takes those arguments. Now, a smart compiler could look at the context and know that the thing you just created would be immediately used in a deleted function, but I doubt that would be compliant. Is it the job of an expression to know the context in which it is placed? – Michael Price Oct 24 '11 at 04:26
  • ideone does not result in that error... it behaves the way you expected it to. Not sure which compiler is correct, unfortunately... the section of the standard on initialization makes my head hurt. – Dennis Zickefoose Oct 24 '11 at 04:42
  • ideone uses 4.5.1... naturally, just when I conclude this was a compiler bug that got fixed, you admit to using a more recent version of the compiler >.< Could still be a bug, though, just in the wrong direction. I'm giving up until somebody smarter than me can chime in one way or the other :) – Dennis Zickefoose Oct 24 '11 at 05:09
  • llvm 3 says "excess elements in struct initializer". I guess its a malformed program and compilers choke on that differently. – Daniel Oct 24 '11 at 17:31

2 Answers2

6

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.

Johannes Schaub - litb
  • 496,577
  • 130
  • 894
  • 1,212
  • I don't follow this part: "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" - doesn't that mean the compiler shouldn't be considering the copy constructor as a candidate, because it requires a user-defined conversion? – HighCommander4 Oct 24 '11 at 22:09
  • @High please check again. I had a thinko in the above. – Johannes Schaub - litb Oct 24 '11 at 22:16
  • @High no because you don't initialize a class C object by `{ ... }`. You initialize it by `( ... )`. If you would do `C c{1};` then what I said applies. But you did put parens around the braces, which causes it to not be a list initialization anymore, but a normal call to a set of functions during direct initialization (in this case, constructors). – Johannes Schaub - litb Oct 24 '11 at 22:23
  • Here's the part that I don't agree with: "Constructing C from {1, 2} also is straight forward and takes your constructor template" - how is it just as straightforward as S{1, 2}, when C{1, 2} requires an additional conversion from the 1 to an S? – HighCommander4 Oct 24 '11 at 22:26
  • Consider `struct B; struct A { operator int(); operator B(); }; struct B { B(int); }; A a; B b{a};`. The declaration of `b` is well-formed, the `B(B&&)` constructor is not used, because it would require a user defined conversion from `A` to `B`. If you say `B b(a);` or `B b({a});` the code becomes ambiguous. – Johannes Schaub - litb Oct 24 '11 at 22:27
  • @High list initializations can have nested user defined conversions. They are not limited like implicit conversions from expressions. – Johannes Schaub - litb Oct 24 '11 at 22:28
  • I guess that's just the way it is... I find it interesting that, for the second example, `C{1}`, `C{{1}}`, and `C(1)` all work, but `C({1})` does not. Oh well. – HighCommander4 Oct 24 '11 at 22:51
  • @High `C(1)` works because `1` to `S` is the only parameter conversion that doesn't need two user defined conversions in a row. And `C{1}` works because for a parameter of a copy/move ctor no user defined conversions are allowed as mentioned above, in this case. It *is* unintuitive I think, but I guess we have to take it as it is :( – Johannes Schaub - litb Oct 25 '11 at 10:57
4

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.

Dennis Zickefoose
  • 10,791
  • 3
  • 29
  • 38
  • Its the C constructor that is templated. Why is it preferring the templated function over the non-templated? – Chris Dodd Oct 24 '11 at 04:36
  • What about the fact that in the construction of `S{1, 2}`, the `1, 2` is a perfect match for the argument list, whereas for the construction of `C{1, 2}`, an implicit conversion from the `1` to an `S` object is required? I thought that would make `S{1, 2}` the better overload. – HighCommander4 Oct 24 '11 at 04:39
  • @Chris: Dennis means the non-templated C constructor (the copy constructor) is preferred over the templated C constructor. – HighCommander4 Oct 24 '11 at 04:40
  • @High: Frankly, I'm starting to conclude that your compiler was buggy to even consider the copy constructor, because it required two user defined conversions in sequence [from 1->S and from {S, 2}->C]. Also, that isn't an example of copy initialization, so the copy constructor shouldn't have been considered in the first place. – Dennis Zickefoose Oct 24 '11 at 05:01
  • But, if it were valid, then both would require an implicit conversion. Either from {S, 2}->C or from {1, 2}->S, making both equally viable. And since one was not a template function, that one got chosen. – Dennis Zickefoose Oct 24 '11 at 05:04