Yes, this is possible, though it does require a macro AFAIK, but you seem to not mind that.
We of course need some SFINAE context because that is the only place where we are allowed to write syntax errors. But creating in-place templates is tricky because local classes cannot contain them, so even if(class { /* template magic */}x; <something with x>)
C++17 feature won't help much.
Since C++14 there is one feature that I am aware of which does allow defining types with templated methods inside an expression - templated lambdas.
I took inspiration from std::visit
with overloaded set and SFINAE test using overloads:
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
// explicit deduction guide (not needed as of C++20)
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
#define HAS_METHOD(class_type,method) \
overloaded { \
[](auto* arg, decltype(&std::decay_t<decltype(*arg)>::method) ptr) constexpr \
{ return true;}, \
[](auto* arg, ...) constexpr \
{ return false;} \
}((class_type*)nullptr,nullptr)
#include <iostream>
int main()
{
struct S{void call();};
if constexpr (HAS_METHOD(S,call))
std::cout << "S has a call\n";
else
std::cout << "S does not have a call\n";
if constexpr (HAS_METHOD(S,call2))
std::cout << "S has a call2\n";
else
std::cout << "S does not have a call2\n";
}
S has a call
S does not have a call2
Explanation
Overload-based SFINAE
Based on this answer, one can base SFINAE on overloading resolution between templated functions. Nice property of this is there is no need for specialization.
Here I used this approach for lambda's templated operator()
. The code boils down to something like this:
struct overload{
template<class T>
bool operator()(T * arg, decltype(&std::decay_t<decltype(*arg)>::call) ptr)
{
return true;
}
template<class T>
bool operator()(T * arg, ...)
{
return false;
}
};
When we call overload{...}((S*)nullptr,nullptr)
, T
is deduced to be S
from the first parameter. This effectivelly gets rids of the templated code while still being in SFINAE context. The first (auxillary) parameter is necessary because lambdas do not have template <typename S>
prior to C++20, also to obtain the type, one has to use decltype(arg)
. std::decay_t
is required because dereferencing a pointer returns a reference and T&::call
is never valid syntax.
Note that one cannot use std::declval
here because the context is evaluated. Pointer it is then, we won't actually dereference it anywhere. Now
- If
S::call
is valid, the second parameter is of type "pointer to a member function with call
's signature". Of course nullptr
is a valid value for any pointer. Because this overload is more specific than ...
(anything valid is), it is chosen and true
is returned in constexpr
manner.
- If
S::call
constitues a syntax error, the first overload is discarded by SFINAE, the second still matches because ...
will match nullptr
and the first argument could still be deduced. In that case we return false
.
Overloading
To build the required set of overloads from lambdas in one expression, one can use parameter pack expansion and inheritance of methods which is exactly what this line from std::visit
helper does:
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
Then the macro itself just construct a temporary instance of this class, ctor is used to initialize the base classes = pass the lambdas. After that, the temporary is immedietely called with ((S*)nullptr,nullptr)
arguments.