Reputation: 3048
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
Reputation:
To expand from the comments:
A thought I had was to add an additional template parameter (call it
DummyType
) tobar()
, which defaults to e.g.DummyType = T1
, and then checkstd::is_same<DummyType, T2>
, but the fact thatbar()
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
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
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");
}
Upvotes: 16