Lukas Barth
Lukas Barth

Reputation: 3048

Use a enable_if that does not depend on method's template parameter

I'm trying to use std::enable_if and SFINAE to switch out the implementation of a class template's method based purely on the template parameters of the class. Example:

#include <type_traits>

template<class T1, class T2>
class Foo {
    template<class InnerT, class ... Args>
    typename std::enable_if<std::is_same<T1, T2>::value, void>::type
    bar(InnerT param) {};

    template<class InnerT, class ... Args>
    typename std::enable_if<!std::is_same<T1, T2>::value, void>::type
    bar(InnerT param) {};
};


int main() {
    Foo<int, int> f;
}

Here, bar() should behave differently based on whether T1 and T2 are the same type or not. However, this code does not compile. Neither GCC nor clang tell me anything useful. I suspect the problem is that the std::enable_if condition does not depend on the parameters of bar(), i.e., not on its immediate context as specified in paragraph 17.8.2, point 8, of the standard.

This assumption is supported by the fact that this code compiles fine:

#include <type_traits>

class DummyClass {};

template<class T1, class T2>
class Foo {
    template<class InnerT, class ... Args>
    typename std::enable_if<std::is_same<T1, T2>::value || 
                            std::is_same<InnerT, DummyClass>::value, void>::type
    bar(InnerT param) {};

    template<class InnerT, class ... Args>
    typename std::enable_if<!std::is_same<T1, T2>::value || 
                            std::is_same<InnerT, DummyClass>::value, void>::type
    bar(InnerT param) {};
};


int main() {
    Foo<int, int> f;
}

Now the expression inside the std::enable_if depends on the "immediate context", namely InnerT, even though that part of the expression always evaluates to false.

It looks like I can use this as a workaround, but that feels really hacky and ugly. How do you solve this problem "correctly"? A thought I had was to add an additional template parameter (call it DummyType) to bar(), which defaults to e.g. DummyType = T1, and then check std::is_same<DummyType, T2>, but the fact that bar() takes a parameter pack makes this impossible (or does it…?)

Upvotes: 15

Views: 2008

Answers (3)

user743382
user743382

Reputation:

To expand from the comments:

A thought I had was to add an additional template parameter (call it DummyType) to bar(), which defaults to e.g. DummyType = T1, and then check std::is_same<DummyType, T2>, but the fact that bar() takes a parameter pack makes this impossible (or does it…?)

It doesn't. Doing exactly what you guessed won't work, will work.

#include <type_traits>

template<class T1, class T2>
struct Foo {
    template<class InnerT, class ... Args, class DummyType = T1>
    typename std::enable_if<std::is_same<DummyType, T2>::value, void>::type
    bar(InnerT param) {};

    template<class InnerT, class ... Args, class DummyType = T1>
    typename std::enable_if<!std::is_same<DummyType, T2>::value, void>::type
    bar(InnerT param) {};
};


int main() {
    Foo<int, int> f;
    f.bar(3);                   // InnerT = int; Args = empty; DummyType = int.
    f.bar<int, void, short>(4); // InnerT = int; Args = void, short; DummyType = int.
}

But if I add the DummyType as second template parameter, and then later pass a list of template arguments that should go into the pack - how does the compiler now that the second argument should not go into DummyType, but be the first thing that's part of Args?

That's why I added as the last parameter. Template non-pack parameters are allowed to follow template pack parameters if they have a default. The compiler will use all explicitly specified arguments for Args, and will use DummyType = T1 no matter which arguments you specify.

Upvotes: 8

max66
max66

Reputation: 66190

I suspect the problem is that the enable_if condition does not depend on the parameters of bar,

Exactly.

A thought I had was to add an additional template parameter (call it DummyType) to bar, which defaults to e.g. DummyType = T1, and then check std::is_same

I usually see exactly this solution.

but the fact that bar takes a parameter pack makes this impossible (or does it…?)

No if you place DummyType before InnerT

   template <typename D1 = T1, typename InnerT, typename ... Args>
   typename std::enable_if<std::is_same<D1, T2>::value>::type
   bar (InnerT param) { std::cout << "- true version" << std::endl; }

   template <typename D1 = T1, typename InnerT, typename ... Args>
   typename std::enable_if<!std::is_same<D1, D2>::value>::type
   bar (InnerT param) { std::cout << "- false version" << std::endl; }

This works perfectly.

The drawback of this solution is that you can "hijack" bar() explicating the D1 type

Foo<int, int> f;

f.bar(0);        // print "- true version"
f.bar<long>(0);  // print "- false version"

but you can solve this problem imposing that T1 is that same as D1

template <typename T1, typename T2>
struct Foo {
   template <typename D1 = T1, typename InnerT, typename ... Args>
   typename std::enable_if<   std::is_same<D1, T2>::value
                           && std::is_same<D1, T1>::value>::type
   bar (InnerT param) { std::cout << "- true version" << std::endl; }

   template <typename D1 = T1, typename InnerT, typename ... Args>
   typename std::enable_if< ! std::is_same<D1, T2>::value
                           && std::is_same<D1, T1>::value>::type
   bar (InnerT param) { std::cout << "- false version" << std::endl; }
};

Now you can't "hijack" bar() anymore.

Upvotes: 2

Caleth
Caleth

Reputation: 62531

Rather than try to SFINAE your way into two implementations, just use normal overload resolution.

#include <type_traits>
#include <iostream>

template<class T1, class T2>
class Foo {
    template<class InnerT, class ... Args>
    void do_bar(InnerT param, std::true_type, Args... args) { std::cout << "same" << std::endl; }

    template<class InnerT, class ... Args>
    void do_bar(InnerT param, std::false_type, Args... args) { std::cout << "not same" << std::endl; }

public:
    template<class InnerT, class ... Args>
    void bar(InnerT&& param, Args&&... args) 
    {
        do_bar(std::forward<InnerT>(param), std::is_same<T1, T2>{}, std::forward<Args>(args)...);
    }

};

int main() {
    Foo<int, int> f1;
    Foo<int, double> f2;

    f1.bar(1, 2, 3);
    f2.bar("Hello");
}

See it live

Upvotes: 16

Related Questions