Ответ 1
Ваше использование указателя на функцию-член - плохая идея; если foo
перегружен, он ложно терпит неудачу (у вас есть foo, но не только один). Кто действительно хочет "у вас есть ровно один foo"? Почти никто.
Вот краткая версия:
template<class T>
using dot_foo_r = decltype( std::declval<T>().foo() );
template<class T>
using can_foo = can_apply<dot_foo_r, T>;
где
namespace details {
template<template<class...>class, class, class...>
struct can_apply:std::false_type{};
template<template<class...>class Z, class...Ts>
struct can_apply<Z, std::void_t<Z<Ts...>>, Ts...>:std::true_type{};
}
template<template<class...>class Z, class...Ts>
using can_apply = details::can_apply<Z, void, Ts...>;
Теперь писать dot_foo_r
немного раздражает.
С constexpr
lambdas мы можем сделать его менее раздражающим и сделать это inline.
#define RETURNS(...) \
noexcept(noexcept(__VA_ARGS__)) \
-> decltype(__VA_ARGS__) \
{ return __VA_ARGS__; }
Для этого нужен макрос RETURNS
, по крайней мере до тех пор, пока сообщение @Barry в [](auto&&f)RETURNS(f())
не будет равно [](auto&&f)=>f()
.
Затем мы пишем can_invoke_f
, который является constexpr
вариантом std::is_invokable
:
template<class F>
constexpr auto can_invoke( F&& f ) {
return [](auto&&...args)->std::is_invokable<F(decltype(args)...)>{
return {};
};
}
Это дает нам:
if constexpr(
can_invoke([](auto&&var) RETURNS(var.foo()))(var)
) {
var.foo();
}
или используя @Barry предложенный синтаксис С++ 20:
if constexpr(can_invoke(var=>var.foo())(var)) {
var.foo();
}
и мы закончили.
Фокус в том, что макрос RETURNS
(или =>
С++ 20) позволяет нам делать SFINAE в выражении. Лямбда становится простым способом переносить это выражение вокруг как значение.
Вы можете написать
[](auto&&var) ->decltype(var.foo()) { return var.foo(); }
но я думаю, что RETURNS
стоит того (и мне не нравятся макросы).