When writing template specialization with SFINAE you often come to the point where you need to write a whole new specialization because of one small not-existing member or function. I would like to pack this selection into a small statement like orElse<T a,T b>
.
small example:
template<typename T> int get(T& v){
return orElse<v.get(),0>();
}
is this possible?
The intent of orElse<v.get(),0>()
is clear enough, but if such a thing could exist,
it would have to be be one of:
Invocation Lineup
orElse(v,&V::get,0)
orElse<V,&V::get>(v,0)
orElse<V,&V::get,0>(v)
where v
is of type V
, and the function template thus instantiated
would be respectively:
Function Template Lineup
template<typename T>
int orElse(T & obj, int(T::pmf*)(), int deflt);
template<typename T, int(T::*)()>
int orElse(T & obj, int deflt);
template<typename T, int(T::*)(), int Default>
int orElse(T & obj);
As you appreciate, no such a thing can exist with the effect that you want.
For any anyone who doesn't get that,
the reason is simply this: None of the function invocations in the Invocation Lineup
will compile if there is no such member as V::get
. There's no getting round
that, and the fact that the function invoked might be an instantiation of a
function template in the Function Template Lineup makes no difference whatever.
If V::get
does not exist, then any code that mentions it will not compile.
However, you seem to have a practical goal that need not be approached
in just this hopeless way. It looks as if, for a given name foo
and an given type R
,
you want to be able to write just one function template:
template<typename T, typename ...Args>
R foo(T && obj, Args &&... args);
which will return the value of R(T::foo)
, called upon obj
with arguments args...
,
if such a member function exists, and otherwise return some default R
.
If that's right, it can be achieved as per the following illustration:
#include <utility>
#include <type_traits>
namespace detail {
template<typename T>
T default_ctor()
{
return T();
}
// SFINAE `R(T::get)` exists
template<typename T, typename R, R(Default)(), typename ...Args>
auto get_or_default(
T && obj,
Args &&... args) ->
std::enable_if_t<
std::is_same<R,decltype(obj.get(std::forward<Args>(args)...))
>::value,R>
{
return obj.get(std::forward<Args>(args)...);
}
// SFINAE `R(T::get)` does not exist
template<typename T, typename R, R(Default)(), typename ...Args>
R get_or_default(...)
{
return Default();
}
} //namespace detail
// This is your universal `int get(T,Args...)`
template<typename T, typename ...Args>
int get(T && obj, Args &&... args)
{
return detail::get_or_default<T&,int,detail::default_ctor>
(obj,std::forward<Args>(args)...);
}
// C++14, trivially adaptable for C++11
which can be tried out with:
#include <iostream>
using namespace std;
struct A
{
A(){};
int get() {
return 1;
}
int get(int i) const {
return i + i;
}
};
struct B
{
double get() {
return 2.2;
}
double get(double d) {
return d * d;
}
};
struct C{};
int main()
{
A const aconst;
A a;
B b;
C c;
cout << get(aconst) << endl; // expect 0
cout << get(a) << endl; // expect 1
cout << get(b) << endl; // expect 0
cout << get(c) << endl; // expect 0
cout << get(a,1) << endl; // expect 2
cout << get(b,2,2) << endl; // expect 0
cout << get(c,3) << endl; // expect 0
cout << get(A(),2) << endl; // expect 4
cout << get(B(),2,2) << endl; // expect 0
cout << get(C(),3) << endl; // expect 0
return 0;
}
There is "compound SFINAE" in play in the complicated return type:
std::enable_if_t<
std::is_same<R,decltype(obj.get(std::forward<Args>(args)...))
>::value,R>
If T::get
does not exist then decltype(obj.get(std::forward<Args>(args)...)
does not compile. But if it does compile, and the return-type of T::get
is
something other than R
, then the std::enable_if_t
type specifier does not
compile. Only if the member function exists and has the desired return type R
can the R(T::get)
exists case be instantiated. Otherwise the
catch-all R(T::get)
does not exist case is chosen.
Notice that get(aconst)
returns 0 and not 1. That's as it should be,
because the non-const overload A::get()
cannot be called on a const A
.
You can use the same pattern for any other R foo(V & v,Args...)
and
existent or non-existent R(V::foo)(Args...)
.
If R
is not default-constructible, or if you want the default R
that
is returned when R(V::foo)
does not exist to be something different from
R()
, then define a function detail::fallback
(or whatever) that returns the
desired default R
and specify it instead of detail::default_ctor
How nice it would be it you could further template-paramaterize the pattern
to accomodate any possible member function of T
with any possible return
type R
. But the additional template parameter you would need for that would
be R(T::*)(typename...)
,and its instantiating value would have to be
&V::get
(or whatever), and then the pattern would
force you into the fatal snare of mentioning the thing whose existence is in doubt.