This totally ruins my fun, while I try to push every dirty thing to left-most.
Here is an example.
Although the below code is not compilable, it is relatively beautiful and easy to read :-
template<bool enable= a&& b> typename std::enable_if_t<enable,void> f(){}
template<bool enable= a&&!b> typename std::enable_if_t<enable,void> f(){}
template<bool enable=!a&& b> typename std::enable_if_t<enable,void> f(){}
template<bool enable=!a&&!b> typename std::enable_if_t<enable,void> f(){}
//^ so neat
look like : if(enable){instantiate static f(){}} ... so intuitive!
... compared to a compilable one :-
template<bool a1=a,bool b1=b> typename std::enable_if_t< a1&& b1,void> f(){}
template<bool a1=a,bool b1=b> typename std::enable_if_t< a1&&!b1,void> f(){}
template<bool a1=a,bool b1=b> typename std::enable_if_t<!a1&& b1,void> f(){}
template<bool a1=a,bool b1=b> typename std::enable_if_t<!a1&&!b1,void> f(){}
^ two temp "type" ----> scroll -------> ^ deep hidden logic
It may seem to be a trivial problem, but it reoccurs to me in many files.
It makes me nervous. I start to code SFINAE with fear.
Are there some ways to make it works and still be concise and intuitive?
Here is coliru demo.
Edit: Here is an example that is more similar to the real world case (beautiful-but-wrong version):-
template<class X,class A,class B,class C,class D>class Database{
public: static constexpr bool hasC=typename X::hasC;
public: static constexpr bool hasD=typename X::hasD;
/** some complex field (NOT depend on "hasC" and "hasD") */
public: template<bool enable=!hasC&&!hasD>
std::enable_if_t<enable,void> add(A a,B b){
/**some complex */
}
public: template<bool enable=hasC&&!hasD>
std::enable_if_t<enable,void> add(A a,B b,C c){
/**some complex */
}
public: template<bool enable=!hasC&&hasD>
std::enable_if_t<enable,void> add(A a,B b,D d){
/**some complex */
}
public: template<bool enable=hasC&&hasD>
std::enable_if_t<enable,void> add(A a,B b,C c,D d){
/**some complex */
}
};
I think that until you get constexpr if with C++17, the most legible way to do this is with tagged dispatch:
template<bool a,bool b>
class Test{
public:
void f()
{
fhelp(f_tag<>{});
}
private:
template<bool = a, bool = b>
struct f_tag{};
void fhelp(f_tag<true, true>){}
void fhelp(f_tag<true, false>){}
void fhelp(f_tag<false, true>){}
void fhelp(f_tag<false, false>){}
};
That way we can be more explicit about what the expected values of a
and b
are. This works well especially since your conditionals are all &&
. It's also more obvious to the naked eye that no two overloads are the same.
The C++17 version would have us write our f()
like so:
void f()
{
if constexpr (a && b)
{
// ...
}
if constexpr (a && !b)
{
// ...
}
if constexpr (!a && b)
{
// ...
}
if constexpr (!a && !b)
{
// ...
}
}
In fact this may not be very legible after all. Personally I still prefer the tagged dispatch approach.
Regarding your "real-world" example where the functions have different parameters, you may start to consider specializing the class instead. However, you can still accomplish what you need with tag dispatch. The public-facing function needs to become a variadic template, while the helper functions retain their true types, so you're still safe:
public:
template<bool = X::hasC, bool = X::hasD>
struct add_tag{};
template<class... T>
void add(T&&... args)
{
add_help(add_tag<>{}, std::forward<T>(args)...);
}
private:
void add_help(add_tag<false, false>, A a, B b)
{/*..*/}
void add_help(add_tag<true, false>, A a, B b, C c)
{/*..*/}
void add_help(add_tag<false, true>, A a, B b, D d)
{/*..*/}
void add_help(add_tag<true, true>, A a, B b, C c, D d)
{/*..*/}