2

The following code complains on the line with dp1 "term does not evaluate to a function taking 0 arguments". I expect that dp1 would match the IsInstantiation specialization of Dispatch, but it errors.

How would I change this code so that it will work as I expect? I am using VS2015 and cannot use std::invoke or std::is_invokable.

#include <type_traits>

template <typename>
class TemplateA {};

struct Callable
{
    void operator()() {};
};

template <typename F, typename... Args>
constexpr auto Invoke(F &&f, Args &&... args)
{
    return f(std::forward(args)...);
}

template <template<typename...> class TT, typename T>
struct IsInstantiation : std::false_type {};

template <template<typename...> class TT, typename... Ts>
struct IsInstantiation<TT, TT<Ts...>> : std::true_type {};

template <typename, typename = void>
struct HasNoArgs : std::false_type {};

template <typename T>
struct HasNoArgs<T, std::void_t<decltype(Invoke(T{}))>> : std::true_type {};

template <typename T1, typename = void>
class Dispatch;

template <typename T1>
class Dispatch<T1, std::enable_if_t<HasNoArgs<T1>::value>> {};

template <typename T1>
class Dispatch<T1, std::enable_if_t<IsInstantiation<TemplateA, T1>::value>> {};

int main(int argc, char *argv[])
{
    Dispatch<TemplateA<int>> dp1;
    Dispatch<Callable> dp2;
    return 0;
}
Graznarak
  • 3,626
  • 4
  • 28
  • 47
  • Related: using INVOKE in C++11: https://stackoverflow.com/q/32918679/1896169 – Justin Apr 13 '18 at 17:25
  • I'd rename `HasNoArgs` to be `CanInvokeWithNoArgs` because you are asking two questions, 1. can we call `Invoke` at all for the type? and 2. can we do it with no arguments? – AndyG Apr 13 '18 at 17:34

1 Answers1

3

There are several problems with your code.

  1. This is an improper use of forward:

    return f(std::forward(args)...);
    

    The function parameter for std::forward is a non-deduced context, on purpose. So this call will never succeed. You want f(std::forward<Args>(args)...)

  2. Invoke is SFINAE unfriendly:

    template <typename F, typename... Args>
    constexpr auto Invoke(F &&f, Args &&... args)
    { ... }
    

    If F is indeed invocable with Args..., this function will work fine. If it isn't, then this is a hard compile error because the problem only surfaces in the body, which is outside of the "immediate context" of the substitution. As a result, it's not a "substitution failure" (the SF in SFINAE), it's an instantiation failure. In HasNoArgs, you're trying to see if you can invoke it - which means you need a substitution failure. To do that, you need to lift the expression into the immediate context:

    template <typename F, typename... Args>
    constexpr auto Invoke(F &&f, Args &&... args)
        -> decltype(f(std::forward<Args>(args)...))
    { return f(std::forward<Args>(args)...); }
    

    Now, it's SFINAE friendly (and additionally, can return a reference).

  3. Your HasNoArgs checks more than you think it does:

    template <typename T>
    struct HasNoArgs<T, std::void_t<decltype(Invoke(T{}))>> : std::true_type {};
    

    This requires that T can be invoked with no arguments, which after fixing #1 and #2 above, is now possible. But it also requires that T{} be a valid expression. Non-default-constructible callables will fail this test, but not for the reason that the test claims it's looking for. To fix that, use std::declval:

    template <typename T>
    struct HasNoArgs<T, std::void_t<decltype(Invoke(std::declval<T>()))>> : std::true_type {};
    
Barry
  • 286,269
  • 29
  • 621
  • 977
  • 1
    I'd probably write `std::forward(f)(std::forward(args)...)` so that it'd call the appropriate ref-qualified `operator()` overload. – Justin Apr 13 '18 at 17:28
  • I usually have the template argument on `std::forward`, I just forgot it here and the compiler didn't catch it. I had used `std::declval` for exactly that reason on an earlier version, but forgot why when I wrote this simple example. – Graznarak Apr 13 '18 at 17:30