15

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…?)

5
  • Well, with C++17 you can always turn bar() into a wrapper that uses if constexpr to invoke one of the two actual alternate methods. Commented Apr 17, 2018 at 11:03
  • 3
    "or does it...?" -- It doesn't. Doing exactly what you guessed won't work, will work.
    – user743382
    Commented Apr 17, 2018 at 11:05
  • 2
    The "correct"(TM) solution would probably be not to force the use of SFINAE, but rather do tag dispatch instead. Commented Apr 17, 2018 at 11:10
  • @hvd: 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? Commented Apr 17, 2018 at 11:12
  • @LukasBarth Don't add it as the second parameter, add it as the last.
    – user743382
    Commented Apr 17, 2018 at 11:27

3 Answers 3

16

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

3
  • 1
    While every C++ std does it, is is_same<int,int> guaranteed to descend from true_type, or is that only true of is_same<int,int>::type? I should check. Commented Apr 17, 2018 at 11:30
  • 7
    "A BinaryTypeTrait describes a relationship between two types. ... It shall be ... publicly and unambiguously derived, directly or indirectly, from its base characteristic, which is a specialization of the template integral_­constant" [meta.rqmts]
    – Caleth
    Commented Apr 17, 2018 at 11:35
  • 2
    thanks! I've used that lots, and had always meant to check. Commented Apr 17, 2018 at 12:02
8

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.

1
  • 2
    Today I learned…! I wasn't aware that defaulted parameters may follow after template packs. Thanks! Commented Apr 17, 2018 at 11:41
2

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.

2
  • Thanks! If I understand that correctly, explicating the D1 type still "hijacks" the dummy, right? It just produces an error then. That's probably not acceptable in my case, since I'm modifying some library's API function, and it would be bad if this introduced compile errors into existing code… The solution by @hvd seems to overcome this, though! Commented Apr 17, 2018 at 11:45
  • 1
    @LukasBarth - "If I understand that correctly, explicating the D1 type still "hijacks" the dummy, right?" Right. But you can impose that D1 is equal to T1. And I'm agree: hvd's solution is better than mine. But give a look also at the Caleth's solution (the tag dispatching one): avoids completely SFINAE and is (IMHO) very elegant.
    – max66
    Commented Apr 17, 2018 at 12:04

Not the answer you're looking for? Browse other questions tagged or ask your own question.