c++language-lawyerc++20c++-concepts

Constraint does not disambiguate function in base class from function in derived class, in clang 18/gcc trunk


The following code compiles (in C++20 mode) on gcc version <= 14, and on clang version <=17, not on clang 18 and gcc trunk (https://godbolt.org/z/hvre4x8Pc):

#include <iostream>
#include <concepts>

struct A {
    void f() { std::cout << "A::f\n"; }
};

template <class T>
struct B : public A {
    // Intention: use B::f if T is int. Use A::f otherwise.
    using A::f;
    void f() requires std::same_as<T, int> {
        std::cout << "B::f";
    }
};

int main() {
    B<int>().f(); // <-- clang 18 and gcc trunk: "error: call to 'f' is ambiguous"
}

Clang's error is the following, and gcc's is very similar:

<source>:18:14: error: call to member function 'f' is ambiguous
   18 |     B<int>().f(); // <-- clang 18: "error: call to member function 'f' is ambiguous"
      |     ~~~~~~~~~^
<source>:5:10: note: candidate function
    5 |     void f() { std::cout << "A::f\n"; }
      |          ^
<source>:12:10: note: candidate function
   12 |     void f() requires std::same_as<T, int> {
      |          ^

Both I, and gcc version <= 14, and clang version <= 17, think that the call seems non-ambiguous because B::f is more constrained than A::f, so B::f can be preferred. But if both compilers generate the error in more recent versions, I do realize that they are probably right and there is something I don't understand...

  1. Which is the correct behavior, according to the standard? Why can't it resolve the call by choosing the more constrained f?

  2. I see that I can make this work by replacing using A::f; by void f() { A::f(); } in B, and this works also in my real world code. But this pattern could potentially be harder to apply if the base class contains many overloads of the function, and/or the function prototypes are complex. Is there a workaround that does not require duplicating all prototypes from the base class into the derived class?


Solution

  • This behavior change is related to CWG2789, which made this example ambiguous.

    Per [over.match.funcs.general]/4, the types of the implicit object parameters of both candidates are considered to be the same in this case, so neither function is better than the other based on the ranking of implicit conversion sequences.

    The only thing distinguishing them, then, is the requires-clause. In C++20 (and C++23) as published, the relevant tiebreaker ([over.match.best.general]/2.6) reads:

    ... a viable function F1 is defined to be a better function than another viable function F2 if [...]

    • F1 and F2 are non-template functions with the same parameter-type-lists, and F1 is more constrained than F2 according to the partial ordering of constraints described in [temp.constr.order]

    (The referenced subclause considers a declaration with associated constraints to be more constrained than one without, as you'd expect.)

    CWG2789 changed this to:

    • F1 and F2 are non-template functions and
      • they have the same non-object-parameter-type-lists, and
      • if they are member functions, both are direct members of the same class, and
      • if both are non-static member functions, they have the same types for their object parameters, and
      • F1 is more constrained than F2 according to the partial ordering of constraints described in [temp.constr.order]

    Since the two f are not direct members of the same class, this bullet no longer applies, and the call is ambiguous.