Search code examples
c++language-lawyeroverloadingvolatileoverload-resolution

Overloading and volatile


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):

  • If we keep only the 1st one, the code compiles.
  • If we keep only the 2nd one, we get an error (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.


Solution

  • 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 type const A. The implicit conversion sequence for that case is the identity sequence; it contains no “conversion” from const A to A.

    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.