Search code examples
c++templatesc++11overload-resolutionuniversal-reference

C++ Overload resolution with universal reference function template which can't be changed


Suppose somewhere in my code is a function foo with a universal reference parameter, which I cannot change:

template<typename T>
auto foo(T&& t) { std::cout<<"general version"<<std::endl; }

Now I want to overload foo for a given class A, and make sure that for any qualifier and reference type of A the overload is called. For this I can brute-forcely provide an overload for all possible qualifications (ignore volatile for now):

auto foo(A & a) { std::cout<<"A&"<<std::endl; }
auto foo(A const& a) { std::cout<<"A const&"<<std::endl; }
auto foo(A && a) { std::cout<<"A &&"<<std::endl; }
auto foo(A const&& a) { std::cout<<"A const&&"<<std::endl; }

Demo. This however scales very badly for more parameters.

Alternatively, I can pass by value, which seems to capture as well all the previous cases:

auto foo(A a) { std::cout<<"A"<<std::endl; }

Demo. Now however large object need to be copied (--at least in principle).

Is there an elegant way around these problems?

Remember that I can't change the universal reference function, so SFINAE and the like is no possibility.


Solution

  • Update for C++20: The answer below remains true for C++11 through C++17, but in C++20 you can do this:

    template <typename T>
        requires std::same_as<std::remove_cvref_t<T>, A>
    auto foo(T&& t) {
        // since this is more constrained than the generic forwarding reference
        // this one should be preferred for foo(A{})
    }
    

    Which you can get using more convenient syntax by creating a named concept:

    template <typename T, typename U>
    concept DecaysTo = std::same_as<U, std::decay_t<T>>;
    
    // longest form
    template <typename T> requires DecaysTo<T, A> void foo(T&&);
    
    // middle form
    template <DecaysTo<A> T> void foo(T&&);
    
    // abbreviated form
    void foo(DecaysTo<A> auto&&);
    

    Honestly, I think you're out of luck here. The typical approaches all fail. You can do...

    SFINAE?

    template <typename T> auto foo(T&& );
    template <typename T,
              typename = only_if_is<T, A>>
    auto foo(T&& );
    
    foo(A{}); // error: ambiguous
    

    Write a class that takes an l-or-rvalue reference?

    template <typename T> lref_or_ref { ... };
        
    template <typename T> auto foo(T&& );
    auto foo(lref_or_ref<A> );
    
    foo(A{}); // calls general, it's a better match
    

    The best you could do is introduce a forwarding function using a chooser:

    template <int I> struct chooser : chooser<I - 1> { };
    template <> struct chooser<0> { };
    
    template <typename T>
    auto bar(T&& t, chooser<0> ) {
        // worst-option, general case
        foo(std::forward<T>(t));
    }
    
    template <typename T,
              typename = only_if_is<T, A>>
    auto bar(T&& t, chooser<1>) {
        // A-specific
    }
    
    template <typename T>
    auto bar(T&& t) {
        bar(std::forward<T>(t), chooser<20>{});
    }
    

    But you mentioned in a comment that this doesn't work for you either.