Почему sfinae on if constexpr не разрешено?

Идиома обнаружения работает следующим образом

template<typename T, typename = void>
struct has_foo {static constexpr bool value = false;};
template<typename T>
struct has_foo<T, std::void_t<decltype(&T::foo)>> {static constexpr bool value = true;};
template<typename T>
constexpr bool has_foo_v = has_foo<T>::value;

И тогда мы можем обнаружить наличие foo в любом типе T.

if constexpr(has_foo_v<decltype(var)>)
    var.foo();

Моя проблема в том, что довольно много набирать (читай: хочу сильно разбить клавиатуру, чтобы напечатать), и я подумал, возможно ли следующее

if constexpr(std::void_t<decltype(&decltype(var)::foo)>(), true)
    var.foo();

Это не так.

Есть ли причина этого?
Более конкретно, какие компромиссы должны быть сделаны, если бы это было разрешено?

Ответы

Ответ 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 стоит того (и мне не нравятся макросы).

Ответ 2

Так как С++ 17 всегда существует обходное решение constexpr lambda, если вам действительно нужно сделать sfinae inline:

#include <utility>

template <class Lambda, class... Ts>
constexpr auto test_sfinae(Lambda lambda, Ts&&...) 
    -> decltype(lambda(std::declval<Ts>()...), bool{}) { return true; }
constexpr bool test_sfinae(...)  { return false; }

template <class T>
constexpr bool bar(T var) {
    if constexpr(test_sfinae([](auto v) -> decltype(v.foo()){}, var))
       return true;
    return false;
}

struct A {
    void foo() {}
};

struct B { };

int main() {
    static_assert(bar(A{}));
    static_assert(!bar(B{}));
}

[live demo]

Ответ 3

Вы также можете уменьшить количество кода, используя std::is_detected.

В вашем примере код будет выглядеть следующим образом:

template <class T>
using has_foo_t = decltype(std::declval<T>().foo());

if constexpr(std::is_detected_v<has_foo_t,decltype(var)>)
  var.foo();