14

I have this minimal expression template library with a multiplication, i.e.

template <typename T, typename U>
struct mul {
    const T &v1;
    const U &v2;
};

template <typename T, typename U>
mul<T, U> operator*(const T &one, const U &two) {
    std::cout << " called: mul<T, U> operator*(const T &one, const T &two)\n";
    return mul<T, U>{one, two};
}

and transpose, i.e.

template <typename T>
struct transpose {
    const T &t;
};

template <typename T>
transpose<T> tran(const T &one) {
    return transpose<T>{one};
}

I will introduce some types A and B, where the latter is a subclass of the former:

template <typename T>
struct A {
    T elem;
};

template <typename T>
struct B : A<T> {
    B(T val) : A<T>{val} {}
};

Then, I can call my expression template library as follows (with an overload for printing to std::cout):

template <typename T, typename U>
std::ostream &operator<<(std::ostream &os, const mul<T, U> &m) {
    os << " unconstrained template \n";
}

int main(int argc, char const *argv[]) {
    B<double> a{2};
    B<double> b{3};
    std::cout << tran(a) * b << "\n";
    return 0;
}

This gives me the output :

called: mul<T, U> operator*(const T &one, const T &two)
unconstrained template 

So far so good. Suppose now that I want a specialized treatment for 'transpose of a variable of type A<T> times a variable of type A<T> for some type T', as I had in my main. To this end, I will introduce

template <typename T>
T operator*(const transpose<A<T>> &one, const A<T> &two) {
    std::cout << " called: T operator*(const A<T> &one, const A<T> &two)\n";
    return one.t.elem * two.elem;
}

I run the same main function as above, and I still get the same output as above (unconstrained template). This is to be expected, since transpose<B<double>> is a completely different type compared to transpose<A<double>>, so overload resolution picks the unconstrained template version of operator*.

(Of course, if I change my variable definitions in main to A instead of B, ADL calls the specialized function and output is called: T operator*(const A<T> &one, const A<T> &two) and 6).

I recently learned about SFINAE, so I expected the following change to the more specific multiplication operator would cause overload resulution to select the specialized function:

template <typename T, typename V>
std::enable_if_t<std::is_base_of<A<T>, V>::value, T> operator*(const transpose<V> &one,
                                                               const V &two) {
    std::cout << " called: std::enable_if_t<std::is_base_of<A<T>, V>::value, T> operator*(const "
                 "transpose<V> &one, const V &two)\n";
    return one.t.elem * two.elem;
}

Even using the SFINAE'd operator* I still get the unconstrained template version. How come? What changes should I make to call the more specialized template function?

Ratan Uday Kumar
  • 5,738
  • 6
  • 35
  • 54
Nibor
  • 1,236
  • 9
  • 23

1 Answers1

12

The problem is that in the SFINAE overload, T is used in a non-deduced context. You're effectively asking the compiler: "Enable this if there exists a T such that A<T> is a base class of V." Existential quantification is a good indicator that what you're asking for cannot be SFINAEd.

You can see this yourself if you disable the unconstrained template, as I did here. This forces the compiler to spell out why the other function is not admissible.

You can solve this by making T available through your A (and thus B) classes, like this:

template <typename T>
struct A {
    using Type = T;
    T elem;
};


template <typename V>
std::enable_if_t<std::is_base_of<A<typename V::Type>, V>::value, typename V::Type> operator*(const transpose<V> &one,
                                                               const V &two) {
    std::cout << " called: std::enable_if_t<std::is_base_of<A<T>, V>::value, T> operator*(const "
                 "transpose<V> &one, const V &two)\n";
    return one.t.elem * two.elem;
}

[Live example]

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
Angew is no longer proud of SO
  • 167,307
  • 17
  • 350
  • 455
  • In fact removing the sfinae, the superfluous template argument then uncommenting the uncontrainted one yield me the correct result. – Guillaume Racicot May 29 '19 at 12:35
  • 1
    @GuillaumeRacicot Yes, that works in the reduced example posted. But note that the OP says that the constrained version should only be used when `V` is derived from `A` for some `A`. Removing the sfinae would make it apply to all transpose-nontranspose pairs. – Angew is no longer proud of SO May 29 '19 at 12:37
  • Reading this SFINAE it should only allow `typename V` to be `B` but given that `std::is_base_of::value` yields `true` if `std::is_same::value` it also allows `A`. Shouldn't there be a check to disallow that, too? If I am understand OP's Intention correctly, that is. – Stack Danny May 29 '19 at 14:00
  • @StackDanny I understood the OP's question as "how do I word SFINAE for a special case that involves `A` or a type derived from `A`?" That's how I read "Suppose now that I want a specialized treatment for 'transpose of a variable of type `A` times a variable of type `A` for some type `T`', as I had in my `main`." Note that it was a `B` in the `main`, actually. – Angew is no longer proud of SO May 29 '19 at 14:02
  • @Angew I just find it weird because if you were to remove `struct B` from the code the SFINAE (which strictly asks for a `std::is_base_of` case) will still be called even though there is no inheritance whatsover going on. I just thoughts that's unintentional. I suppose this is to be blamed on the implementation of `std::is_base_of`. – Stack Danny May 29 '19 at 14:13
  • @Angew You interpreted my intent correctly, although more generally, `A` would be an abstract class. – Nibor May 29 '19 at 14:32
  • @StackDanny `std::is_base_of` effectively models IS-A. To be fair, it's much more common in practice to need to know "are you X or one of its descendants" than "are you derived from X, but not X itself." – Angew is no longer proud of SO May 29 '19 at 14:47