c++overloadingc++-concepts

Why are disabled overloads required to be unique?


The following class has four overloads of function f. When T!=int, all overloads have unique parameter lists, but when T=int, all overloads have the same parameter lists. To make it compile even when int and T are identical, I tried to disable three of the functions using a concept. Despite that, the compiler generates an error because the three disabled overloads have identical signatures (https://godbolt.org/z/8aeWdrTs6):

#include <concepts>
template <class T>
struct S {
    void f(T, T) { /*...*/ }
    void f(T, int) requires (!std::same_as<int, T>) { /*...*/ }
    void f(int, T) requires (!std::same_as<int, T>) { /*...*/ }
    void f(int, int) requires (!std::same_as<int, T>) { /*...*/ }
};

int main() {
    S<int> s;
}

clang says error: multiple overloads of 'f' instantiate to the same signature 'void (int, int)', and gcc says something similar.

I can work around this by introducing artificial syntactic differences in the constraints, e.g. (https://godbolt.org/z/TajvPYPo4):

    void f(int, T) requires (!std::same_as<int, T> && true) { /*...*/ }
    void f(int, int) requires (!std::same_as<int, T> && true && true) { /*...*/ }
  1. Why is the language specified so that disabled overloads must have unique signatures? I understand that all viable overloads must be unique because the compiler must know which one to invoke. But non-viable overloads are not invoked, so requiring uniqueness seems unnecessary in this case.
  2. Is there a better/nicer/more elegant workaround, for example, one that is easier to scale when there are many overloads?

Solution

  • Unelegant and crude, but we could also use SFINAE tricks to disable certain overloads:

    #include <concepts>
    #include <string>
    
    template <class T>
    struct S {
        void f(T, T) { /*...*/ }
        template<typename E = T>
        auto f(E, int) -> std::enable_if_t<std::is_convertible_v<E, T> && !std::is_same_v<int, T>, void> { /*...*/ }
        template<typename E = T>
        auto f(int, E) -> std::enable_if_t<std::is_convertible_v<E, T> && !std::is_same_v<int, T>, void> { /*...*/ }
        template<typename E = T>
        auto f(int, int) -> std::enable_if_t<std::is_convertible_v<E, T> && !std::is_same_v<int, T>, void> { /*...*/ }
    };
    
    int main() {
        S<int> s;
        s.f(0, 0);
    
        S<std::string> sd;
        sd.f("hello", "world");
        sd.f("hello", 0);
        sd.f(0, "world");
        sd.f(0, 0);
    }
    

    This works because the overloads using E are templates and are not instantiated until overload.

    https://godbolt.org/z/Pdq3MYPnc