21

The detection idiom works as follows

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;

And then we can detect the presence of foo in any type T.

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

My problem is, that is quite a lot to type (read: want to smash my keyboard a lot to type), and I wondered if the following is possible

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

It is not.

Is there a reason behind this?
More specifically, what trade-offs will have to be made if this were allowed?

Passer By
  • 19,325
  • 6
  • 49
  • 96
  • 9
    [Relevant](https://meta.stackoverflow.com/a/323382/2069064). One answer might be because there's no substitution here, so how can there be SFINAE? Another answer might be because it wasn't considered since the proposals were always about the `if` taking a `bool`? – Barry Jun 06 '17 at 16:36
  • @Barry Hopefully edited question into a form that can have a clear answer. Was originally thinking about something along the lines of [this kind of answer](https://stackoverflow.com/a/6623089/4832499) – Passer By Jun 06 '17 at 16:48

3 Answers3

17

Since c++17 there is always a constexpr lambda workaround if you really need to do 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]

W.F.
  • 13,888
  • 2
  • 34
  • 81
  • Note that `declval()` is an lvalue reference, so if the function you test does not accept non-const lvalue reference, the test will fail. – Igor R. Jun 11 '17 at 19:45
  • @IgorR. You're right but we have the adventage that we're creating the lambda `test_sfinae` will test. Still I'll have to think about it as it's far from being perfect. – W.F. Jun 11 '17 at 20:20
  • 1
    @IgorR. I removed the reference in `declval` now it is up to lambda creator if he'd like to operate on references or on values. If the copy constructor is explicitly deleted lambda will match only when accepting reference i.e. `[](auto &v)`, `[](const auto &v)` or `[](auto &&v)` – W.F. Jun 12 '17 at 10:39
8

Your use of pointer to member function is a bad idea; if foo is overloaded, it spuriously fails (you have a foo, but not just one). Who really wants "do you have exactly one foo"? Almost nobody.

Here is a briefer version:

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

template<class T>
using can_foo = can_apply<dot_foo_r, T>;

where

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...>;

Now, writing dot_foo_r is a bit annoying.

With constexpr lambdas we can make it less annoying and do it inline.

#define RETURNS(...) \
  noexcept(noexcept(__VA_ARGS__)) \
  -> decltype(__VA_ARGS__) \
  { return __VA_ARGS__; }

It does need the RETURNS macro, at least until @Barry's submission to [](auto&&f)RETURNS(f()) be equivalent to [](auto&&f)=>f().

We then write can_invoke, which is a constexpr variant of std::is_invocable:

template<class F>
constexpr auto can_invoke( F&& f ) {
  return [](auto&&...args)->std::is_invocable<F(decltype(args)...)>{
    return {};
  };
}

This gives us:

if constexpr(
  can_invoke([](auto&&var) RETURNS(var.foo()))(var)
) {
  var.foo();
}

or using @Barry's proposed C++20 syntax:

if constexpr(can_invoke(var=>var.foo())(var)) {
  var.foo();
}

and we are done.

The trick is that RETURNS macro (or => C++20 feature) lets us do SFINAE on an expression. The lambda becomes an easy way to carry that expression around as a value.

You could write

    [](auto&&var) ->decltype(var.foo()) { return var.foo(); }

but I think RETURNS is worth it (and I don't like macros).

Pascal H.
  • 1,431
  • 8
  • 18
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • 2
    Was just thinking about including this case as a different kind of motivating example in the next draft. `if constexpr(can_invoke_f(x => x.foo())) { ... }` still isn't exactly ideal compared to what OP wanted, but it's not bad. – Barry Jun 06 '17 at 18:39
  • @Barry Got rid of a `decltype` and improved `can_invoke`; `can_invoke` now takes a function instance `f` and returns a `constexpr` invoke-tester on `f`. Freel free to steal/adapt; I think it is much slicker now. – Yakk - Adam Nevraumont Jun 06 '17 at 18:47
  • 3
    What's the `=>` syntax? – Johannes Schaub - litb Jun 07 '17 at 10:15
  • I'm also quite excited about the new syntax. Is the proposal publicly available somewhere? – W.F. Jun 07 '17 at 11:38
  • 1
    @jona http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0573r0.html -- that version was not accepted, I believe Barry is revising it. I believe the `=>` part was viewed positively, other parts (like unary `>>`) less so. – Yakk - Adam Nevraumont Jun 07 '17 at 12:49
  • 1
    @w.f. ping, see above – Yakk - Adam Nevraumont Jun 07 '17 at 12:49
3

You can also reduce the amount of code by using std::experimental::is_detected.

In your example, the code would then look like:

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

if constexpr(is_detected_v<has_foo_t,decltype(var)>)
  var.foo();
SU3
  • 5,064
  • 3
  • 35
  • 66