Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Resolving CRTP function overload ambiguity

I have several functions that I would like to work for derived classes of a CRTP base class. The issue is that if I pass the derived classes into the free functions meant for the CRTP class, ambiguities arise. A minimal example to illustrate this is this code:

template<typename T>
struct A{};

struct C : public A<C>{};

struct B{};

template<typename T, typename U>
void fn(const A<T>& a, const A<U>& b) 
{
    std::cout << "LT, RT\n";
}

template<typename T, typename U>
void fn(const T a, const A<U>& b)
{
    std::cout << "L, RT\n";
}

template<typename T, typename U>
void fn(const A<T>& a, const U& b)
{
    std::cout << "LT, R\n";
}

int main()
{
    C a; // if we change C to A<C> everything works fine
    B b;
    fn(a,a); // fails to compile due to ambiguous call
    fn(b,a);
    fn(a,b);
    return 0;
}

Ideally I would like this to work for the derived classes as it would if I were to use the base class (without having to redefine everything for the base classes, the whole point of the CRTP idiom was to not have to define fn for multiple classes).

like image 369
lightxbulb Avatar asked Jul 10 '19 12:07

lightxbulb


2 Answers

First, you need a trait to see if something is A-like. You cannot just use is_base_of here since you don't know which A will be inherited from. We need to use an extra indirection:

template <typename T>
auto is_A_impl(A<T> const&) -> std::true_type;
auto is_A_impl(...) -> std::false_type;

template <typename T>
using is_A = decltype(is_A_impl(std::declval<T>()));

Now, we can use this trait to write our three overloads: both A, only left A, and only right A:

#define REQUIRES(...) std::enable_if_t<(__VA_ARGS__), int> = 0

// both A
template <typename T, typename U, REQUIRES(is_A<T>() && is_A<U>())
void fn(T const&, U const&);

// left A
template <typename T, typename U, REQUIRES(is_A<T>() && !is_A<U>())
void fn(T const&, U const&);

// right A
template <typename T, typename U, REQUIRES(!is_A<T>() && is_A<U>())
void fn(T const&, U const&);

Note that I'm just taking T and U here, we don't necessarily want to downcast and lose information.


One of the nice things about concepts coming up in C++20 is how much easier it is to write this. Both the trait, which now becomes a concept:

template <typename T> void is_A_impl(A<T> const&);

template <typename T>
concept ALike = requires(T const& t) { is_A_impl(t); }

And the three overloads:

// both A
template <ALike T, ALike U>
void fn(T const&, U const&);

// left A
template <ALike T, typename U>
void fn(T const&, U const&);

// right A
template <typename T, ALike U>
void fn(T const&, U const&);

The language rules already enforce that the "both A" overload is preferred when it's viable. Good stuff.

like image 160
Barry Avatar answered Nov 15 '22 07:11

Barry


Given that in your example the first element of the second function and the second element of the third should not inherit from the CRTP you can try something like the following:

#include<iostream>
#include<type_traits>

template<typename T>
struct A{};

struct C : public A<C>{};

struct B{};

template<typename T, typename U>
void fn(const A<T>& a, const A<U>& b) 
{
    std::cout << "LT, RT\n";
}

template<typename U>
struct isNotCrtp{
    static constexpr bool value = !std::is_base_of<A<U>, U>::value; 
};

template<typename T, typename U, std::enable_if_t<isNotCrtp<T>::value, int> = 0>
void fn(const T a, const A<U>& b)
{
    std::cout << "L, RT\n";
}

template<typename T, typename U, std::enable_if_t<isNotCrtp<U>::value, int> = 0>
void fn(const A<T>& a, const U& b)
{
    std::cout << "LT, R\n";
}

int main()
{
    C a; 
    B b;
    fn(a,a); 
    fn(b,a);
    fn(a,b);
    return 0;
}

Basically we disable the second and third functions when passing a CRTP in first and second argument, leaving only the first function available.

Edit: answering to OP comment, if T and U both inherit the first will be called, wasn't this the expected behavior?

Play with the code at: https://godbolt.org/z/ZA8hZz

Edit: For a more general answer, please refer to the one posted by user Barry

like image 25
CuriouslyRecurringThoughts Avatar answered Nov 15 '22 07:11

CuriouslyRecurringThoughts