I have problems with GCC compiling enable_if
s applied to return value of the templated class method. With Clang, I am able to use an expression in enable_if
on the enum
template argument, while GCC refuses to compile this code.
Here is the problem description, initial code, and its subsequent modifications that try to satisfy me and compilers (unfortunately, not simultaneously).
I have a non-templated class Logic
that contains a templated class method computeThings()
which has an enum Strategy
as one of its template parameters. The logic in computeThings()
depends on the compile-time Strategy
, so if constexpr
is a reasonable way to make an implementation.
#include <iostream>
class Logic {
public:
enum Strategy { strat_A, strat_B };
// class A and class B are dummy in this example, provided to show that there are several template
// parameters, and strategy selection effectively results in
// partial (not full) templated method specification
template <class A, class B, Strategy strategy>
int computeThings();
};
template <class A, class B, Logic::Strategy strategy>
int Logic::computeThings() {
if constexpr(strategy==strat_A)
return 0;
else
return 1;
}
int main() {
Logic mylogic;
std::cout<<mylogic.computeThings<int,int,Logic::strat_A>()<<std::endl; //outputs 0
std::cout<<mylogic.computeThings<int,int,Logic::strat_B>()<<std::endl; //outputs 1
return 0;
}
Variant 1 works fine and compiles both in clang and gcc. However, I want to get rid of if constexpr
and split computeThings()
into two specialized methods based on the chosen Strategy
. Reason: the function is performance-critical and contains a lot of code.
So, I am coming up with Variant 2 that uses enable_if
applied to the return value.
#include <iostream>
class Logic {
public:
enum Strategy { strat_A, strat_B };
template <class A, class B, Logic::Strategy strategy>
typename std::enable_if_t<strategy==Logic::strat_A,int>
computeThings();
template <class A, class B, Logic::Strategy strategy>
typename std::enable_if_t<strategy==Logic::strat_B,int>
computeThings();
};
template <class A, class B, Logic::Strategy strategy>
typename std::enable_if_t<strategy==Logic::strat_A,int>
Logic::computeThings() {
return 0;
}
template <class A, class B, Logic::Strategy strategy>
typename std::enable_if_t<strategy==Logic::strat_B,int>
Logic::computeThings() {
return 1;
}
int main() {
Logic mylogic;
std::cout<<mylogic.computeThings<int,int,Logic::strat_A>()<<std::endl; //outputs 0
std::cout<<mylogic.computeThings<int,int,Logic::strat_B>()<<std::endl; //outputs 1
return 0;
}
I am perfectly comfortable with variant 2 (though would appreciate feedback as well). This code compiles fine using AppleClang (and probably Clang, in general) and produces the right results. However, it fails to compile with GCC with the following error (+ the same but for the other method):
error: prototype for 'std::enable_if_t<(strategy == Logic:: strat_A),int> Logic::computeThings()' does not match any in class 'Logic' Logic::computeThings()
candidates are: template<class A, class B, Logic::Strategy strategy> std::enable_if_t<(strategy == strat_B), int> Logic::computeThings() computeThings();
candidates are: template<class A, class B, Logic::Strategy strategy> std::enable_if_t<(strategy == strat_A), int> Logic::computeThings() computeThings();
So, apparently, using a simple strategy==Logic::strat_A
conflicts with GCC. So, I came up with a solution to this that satisfies both clang and gcc, which wraps strategy==Logic::strat_A
into a struct
:
#include <iostream>
class Logic {
public:
enum Strategy { strat_A, strat_B };
template <Logic::Strategy strategy> struct isStratA {
static const bool value = strategy==Logic::strat_A;
};
template <class A, class B, Logic::Strategy strategy>
typename std::enable_if_t<Logic::isStratA<strategy>::value,int>
computeThings();
template <class A, class B, Logic::Strategy strategy>
typename std::enable_if_t<!Logic::isStratA<strategy>::value,int>
computeThings();
};
template <class A, class B, Logic::Strategy strategy>
typename std::enable_if_t<Logic::isStratA<strategy>::value,int>
Logic::computeThings() {
return 0;
}
template <class A, class B, Logic::Strategy strategy>
typename std::enable_if_t<!Logic::isStratA<strategy>::value,int>
Logic::computeThings() {
return 1;
}
int main() {
Logic mylogic;
std::cout<<mylogic.computeThings<int,int,Logic::strat_A>()<<std::endl; //outputs 0
std::cout<<mylogic.computeThings<int,int,Logic::strat_B>()<<std::endl; //outputs 1
return 0;
}
with Variant 3, both Clang and GCC are happy. However, I am not, as I have to create a lot of dummy wrappers for an unknown reason (here, I have just one, but technically, I should have both isStratA<>
and isStratB<>
).
Questions:
(if that matters, GCC 7.4.0 and Apple LLVM version 10.0.0: clang-1000.11.45.5)
As @bogdan said in the comments, this is most likely a compiler bug. Actually I noticed that it works if you use trailing return types in the out-of-line definitions of your function templates:
template <class A, class B, Logic::Strategy strategy>
auto Logic::computeThings() ->
std::enable_if_t<strategy==Logic::strat_A,int> {
return 0;
}
template <class A, class B, Logic::Strategy strategy>
auto Logic::computeThings() ->
std::enable_if_t<strategy==Logic::strat_B,int> {
return 1;
}
I prefer putting the enable_if
in the type of a non-type template parameter with a default argument:
template <class A, class B, Logic::Strategy strategy,
std::enable_if_t<strategy==Logic::strat_A,int> = 0>
int Logic::computeThings() {
return 0;
}
template <class A, class B, Logic::Strategy strategy,
std::enable_if_t<strategy==Logic::strat_B,int> = 0>
int Logic::computeThings() {
return 1;
}
But SFINAE is far too complex of a feature for something so simple. There are much easier ways to do what you are trying to do. Take this example using tag dispatch:
#include <iostream>
#include <type_traits>
class Logic {
public:
enum Strategy { strat_A, strat_B };
template <class A, class B>
int computeThings(std::integral_constant<Strategy, strat_A>);
template <class A, class B>
int computeThings(std::integral_constant<Strategy, strat_B>);
};
template <class A, class B>
int Logic::computeThings(std::integral_constant<Strategy, strat_A>) {
return 0;
}
template <class A, class B>
int Logic::computeThings(std::integral_constant<Strategy, strat_B>) {
return 1;
}
int main() {
Logic mylogic;
std::cout<<mylogic.computeThings<int,int>(
std::integral_constant<Logic::Strategy, Logic::strat_A>{}
)<<std::endl; //outputs 0
std::cout<<mylogic.computeThings<int,int>(
std::integral_constant<Logic::Strategy, Logic::strat_B>{}
)<<std::endl; //outputs 1
return 0;
}
That can be simplified further by getting rid of the enum and directly defining some tag types instead:
class Logic {
public:
class strat_A {};
class strat_B {};
template <class A, class B>
int computeThings(strat_A);
template <class A, class B>
int computeThings(strat_B);
};
template <class A, class B>
int Logic::computeThings(strat_A) { return 0; }
template <class A, class B>
int Logic::computeThings(strat_B) { return 1; }
int main() {
Logic mylogic;
std::cout<<mylogic.computeThings<int,int>(Logic::strat_A{})<<std::endl; //outputs 0
std::cout<<mylogic.computeThings<int,int>(Logic::strat_B{})<<std::endl; //outputs 1
return 0;
}
A more idiomatic and structured approach to the strategy pattern would be to lift the behaviour of the different strategies out of the computeThings
function and into the strategy classes themselves:
class Logic {
public:
struct strat_A {
template <class A, class B>
static int computeThings(Logic* self);
};
struct strat_B {
template <class A, class B>
static int computeThings(Logic* self);
};
template <class A, class B, class Strategy>
int computeThings() {
return Strategy::template computeThings<A, B>(this);
}
};
template <class A, class B>
int Logic::strat_A::computeThings(Logic* self) {
return 0;
}
template <class A, class B>
int Logic::strat_B::computeThings(Logic* self) {
return 1;
}
int main() {
Logic mylogic;
std::cout<<mylogic.computeThings<int,int,Logic::strat_A>()<<std::endl; //outputs 0
std::cout<<mylogic.computeThings<int,int,Logic::strat_B>()<<std::endl; //outputs 1
return 0;
}
The Logic* self
pointer isn't needed in this example, but would be if the strategies need to access the Logic
instance.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With