Reputation: 2836
How is this expected to work?
struct S {};
void foo(volatile S&);
void foo(S);
int main() {
volatile S v;
foo(v);
}
Compilers disagree on it: MSVC accepts the code, while Clang and GCC says the call is ambiguous (demo).
We can prove that only the 1st candidate can accept v
, if we comment out one of the candidates (and the compilers agree in this case):
S
doesn't have a copy ctor. from volatile S&
).EDIT: Since the question was posted, people came up with a simpler example without volatile
.
Upvotes: 6
Views: 301
Reputation: 119382
The wording that we currently have in the standard about implicit conversion sequences is not great, which is probably why you see this implementation divergence.
Consider [over.best.ics.general] paragraph 1 and the first half of paragraph 6:
An implicit conversion sequence is a sequence of conversions used to convert an argument in a function call to the type of the corresponding parameter of the function being called. The sequence of conversions is an implicit conversion as defined in [conv], which means it is governed by the rules for initialization of an object or reference by a single expression ([dcl.init], [dcl.init.ref]).
[...]
When the parameter type is not a reference, the implicit conversion sequence models a copy-initialization of the parameter from the argument expression. The implicit conversion sequence is the one required to convert the argument expression to a prvalue of the type of the parameter.
This suggests that the implicit conversion sequence from v
(an lvalue of type volatile S
) to the parameter type S
is determined by the rules of copy-initialization of an S
object from a volatile S
lvalue. This is governed by [dcl.init.general]/16.6.2 since the cv-unqualified version of the type of the initializer is the same as the class type of the object being initialized. Because the overload resolution fails (S
has no volatile-qualified copy constructor) sub-sub-bullet 3 applies, and the initialization is ill-formed.
However, when [dcl.init] says that the initialization is ill-formed, and it is a hypothetical initialization for the purpose of forming an implicit conversion sequence, it sometimes means that there is no implicit conversion sequence, and it sometimes means the implicit conversion sequence exists but actually using it in a call would be ill-formed, and you sort of just have to know which one it means. (See CWG2525.)
This matters because if there is no implicit conversion sequence, then the other overload must be selected (the one with a parameter type of volatile S&
), but if there is an implicit conversion sequence that would be ill-formed if used for a call, then the overload resolution is ambiguous.
The example in the second half of [over.best.ics.general] paragraph 6 hints weakly that the latter interpretation is correct:
A parameter of type
A
can be initialized from an argument of typeconst A
. The implicit conversion sequence for that case is the identity sequence; it contains no “conversion” fromconst A
toA
.
A
could theoretically be a class type whose copy constructor has the form A(A&)
(rather than A(const A&)
) yet the example is claiming that the implicit conversion sequence is always the identity conversion.
Paragraph 7 is also a weak hint:
When the parameter has a class type and the argument expression has the same type, the implicit conversion sequence is an identity conversion. When the parameter has a class type and the argument expression has a derived class type, the implicit conversion sequence is a derived-to-base conversion from the derived class to the base class. A derived-to-base conversion has Conversion rank ([over.ics.scs]).
The first sentence doesn't apply because the parameter type S
is not the same as the type of the argument, volatile S
. But the second sentence hints that even if e.g. the copy-initialization of the parameter type from the argument type would not be a simple call to the parameter type's copy constructor (but could actually involve other competing constructors, and may be ambiguous) it doesn't affect the formation of the implicit conversion sequence. So we are led to think that the formation of an implicit conversion sequence to Base
from cv Derived
always succeeds. If that's the case, it ought to be even more the case for a conversion to S
from cv S
(and perhaps the first sentence should be amended to include the cv-qualified case).
So I think Clang and GCC are doing what the standard means, but it's honestly not that clear.
Upvotes: 2
Reputation: 40891
Consider this note [over.best.ics.general]p2:
[Note 1: [...]. So, although an implicit conversion sequence can be defined for a given argument-parameter pair, the conversion from the argument to the parameter might still be ill-formed in the final analysis. — end note]
From [over.best.ics.general]p7:
When the parameter has a class type and the argument expression has the same type, the implicit conversion sequence is an identity conversion.
We know that the ICS for the first argument of the second overload (void foo(S);
) is the identity conversion (even though the modelled copy-initialization isn't possible).
A volatile
S
lvalue -> volatile S&
for the first argument of the other overload is also an identity conversion (written out in [over.ics.ref]p(1.2)).
Since both ICSs are identical, both overloads are viable but neither is the best, making it ambiguous.
Upvotes: 5
Reputation: 63039
Clang and GCC are correct, this is an ambiguous call.
Both overloads of f
are viable functions for that call, because one involves no conversion, and the other is an lvalue-to-rvalue conversion. Neither is a better conversion, because lvalue-to-rvalue has the same rank as identity.
That there isn't a copy constructor that accepts volatile S &
isn't material to overload resolution where there is an exact match in type.
You can see a similar ambiguity with a deleted copy constructor, however this time MSVC agrees with GCC and Clang:
struct S {
S() {}
S(const S&) = delete;
};
This case shows you why the overload resolution rules don't require the conversion to be well-formed. When you = delete
an overloaded function, you want it to be picked, and then be found to be ill-formed.
Upvotes: 2