6

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

lightxbulb
  • 1,251
  • 12
  • 29
  • @CuriouslyRecurringThoughts In my case they are not inherited from the base class, so thank you for the solution, you can formulate it as an answer so I can accept it. However that got me curious, what would be the implications if T and U are also inherited from CRTP. – lightxbulb Jul 10 '19 at 12:35
  • 2
    Why do you need multiple overloads? – Barry Jul 10 '19 at 12:40
  • @Barry In my use case I am defining an `operator*` for arrays. I have 3 options: componentwise multiplication of 2 arrays, and left and right scalar with array multiplication. So the 2 other overloads are for the scalar multiplication. – lightxbulb Jul 10 '19 at 12:43

3 Answers3

6

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.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • I don't think there's a way to deal with a case like `X` inherits `A`, `Y` inherits `A`, and `Z` inherits both `X` and `Y`. But probably this would be an unexpected and broken use of most CRTP classes, since its members that don't depend on the template parameter also become ambiguous in `Z`. – aschepler Jul 10 '19 at 12:59
  • @aschepler I would consider `Z` simply not `A`-like, that's just a bad type for this model. – Barry Jul 10 '19 at 13:00
  • @aschepler I wish there was a way to call that ill-formed somehow (rather than true or false) but I'm not sure there's a way to do that without reflection. – Barry Jul 10 '19 at 13:10
  • Could you elaborate on the three dots choice for the function returning `false_type`? As I understand it you want to get `false_type` as long as you get anything that is not `A` or a child of `A`, right? Why does it have to accept multiple arguments however, or is this just for brevity? Also there's a minor typo, where you forgot to close the `>` in the templates after `REQUIRE`. – lightxbulb Jul 10 '19 at 19:54
  • The `...` is not there to allow multiple arguments, but since it's just a "private" helper declaration for the trait to actually be used, it doesn't matter that it could. It's true `template auto is_A_impl(const T&) -> std::false_type;` would work just as well here. ... – aschepler Jul 11 '19 at 04:14
  • ... But besides being shorter, using `...` in a set of function overloads used for dispatch like this is a common pattern because overload resolution considers an argument that "matches the ellipsis" to be the absolute worst in order of preference for that argument. So no matter how complicated your overload set gets, you know the `...` will be used only if all the other overloads are not viable, essentially a "default" case. – aschepler Jul 11 '19 at 04:16
  • 1
    @aschepler No, `const T&` would not work - that would be a better match for derived types. `...` is necessary as the guaranteed worst match. – Barry Jul 11 '19 at 07:25
2

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

0

This is one of those situations when it's convenient to create a helper class that can be partially specialized to do this, with the function turned into a wrapper that selects the appropriate specialization:

#include <iostream>

template<typename T>
struct A{};

struct C : public A<C>{};

struct B{};

template<typename T, typename U>
struct fn_helper {

    static void fn(const T &a, const U &b)
    {
        std::cout << "L, R\n";
    }
};

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

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

template<typename T, typename U>
struct fn_helper<A<T>, A<U>> {
    static 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 U &b)
{
    fn_helper<T,U>::fn(a, b);
}

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

Output (gcc 9):

LT, RT
L, RT
LT, R

I would expect modern C++ compilers to require selecting only their most modest optimization level to completely optimize away the wrapping function call.

Sam Varshavchik
  • 114,536
  • 5
  • 94
  • 148
  • Can you elaborate on why this works? Is this downcasting the derived class when trying to instantiate the class? – lightxbulb Jul 10 '19 at 12:38
  • All you did was add a new overload. The indirection through `fn_helper` doesn't achieve anything. – Barry Jul 10 '19 at 12:39
  • @Barry What do you mean? The example seems to compile from what I can see, even if one sets `A` to `C`. – lightxbulb Jul 10 '19 at 12:41
  • @lightxbulb It compiles because it has one more overload than your example - the one that takes two arguments of any types. – Barry Jul 10 '19 at 12:41
  • @Barry Yes, I guess that would pose a problem for overloading the `operator*`. – lightxbulb Jul 10 '19 at 12:47