apramc
apramc

Reputation: 1386

How to SFINAE using the function signature of a template function

I have a code that takes a function and executes it based on the function signature like below:

template <int Num>
struct Value {
  int value[Num];
};

struct Executor {
    template <int N>
    void do_exec(std::vector<Value<N>>& n, void (&func) (Value<N>&)) {
        for (auto& item : n)
            func(item);
    }

    template <int N>
    void do_exec(std::vector<Value<N>>& n, void (&func) (Value<N>&, int)) {
        for (int i = 0; i != n.size(); i++)
            func(n[i], i);
    }
};

when the user passes in one of the following functions, the Executor run the do_exec() that matches its signatures.

template <int N>
void f1(Value<N>& item)
{
    for (auto& i : item.value) {
        i = 123;
    }
}

template <int N>
void f2(Value<N>& item, int d)
{
    for (auto& i : item.value) {
        i = d;
    }
}

int main()
{
    Executor exec;
    std::vector<Value<3>> vec(10);
    exec.do_exec(vec, f1);
}

I would like to extend this code, so it can take lambda functions since in the real code, almost all of the agents will call this with GENERIC lambdas.

I tried replacing the functors with std::function, but it failed since lambda is not a std::function and type deduction didn't really happen.

then I tried to take two template arguments and SFINAE out the one that doesn't match the signature like below:

template <typename Fn, typename T, typename = void>
struct HasIndex : std::false_type {};   

template <typename Fn, typename T>
struct HasIndex<Fn, T, std::void_t<std::invoke_result_t<Fn, T&, int>>> : std::true_type {};

struct Executor {
    template <int N, typename Fn, std::enable_if_t<!HasIndex<Fn, Value<N>>::value, int> = 1>
    void do_exec(std::vector<Value<N>>& n, Fn func) {
        for (auto& item : n)
            func(item);
    }

    template <int N, typename Fn, std::enable_if_t<HasIndex<Fn, Value<N>>::value, int> = 1>
    void do_exec(std::vector<Value<N>>& n, Fn func) {
        for (int i = 0; i != n.size(); i++)
            func(n[i], i);
    }
}; 

this didn't work either, since the functions that executor will take are ALWAYS template functions (GENERIC Lambda). I don't know exactly how to approach this problem, any help appreciated.

c++14 solution please (I know invoke_result is c++ 17)

https://godbolt.org/z/W7z3Mv

Upvotes: 1

Views: 617

Answers (2)

alter_igel
alter_igel

Reputation: 7212

The fix is fairly simple. Firstly, I would use std::is_invocable_v from the type traits library to test for compatible function signatures in the SFINAE mechanism. Line breaks keep the template signatures readable, I find:

template<
    int N,
    typename Fn,
    std::enable_if_t<std::is_invocable_v<Fn, Value<N>&>>* = nullptr
>
void do_exec(std::vector<Value<N>>& n, Fn func) {
    [...]
}

template<
    int N,
    typename Fn,
    std::enable_if_t<std::is_invocable_v<Fn, Value<N>&, int>>* = nullptr
>
void do_exec(std::vector<Value<N>>& n, Fn func) {
    [...]
}

This allows non-template references to functions and generic lambdas, but the following won't yet work:

template <int N>
void f1(Value<N>& item){ [...] }

int main(){
    Executor exec;
    std::vector<Value<3>> vec(10);
    exec.do_exec(vec, f1);
}

For me, this fails with a pretty generic template argument deduction/substitution failure. To make this work, you need to specialize f1 with a value for N, as in:

int main(){
    Executor exec;
    std::vector<Value<3>> vec(10);
    exec.do_exec(vec, f1<3>); // Fn is deduced as void(&)(Value<3>&) (I think)
}

Live Demo


UPDATE for C++14-compatibility

Since std::is_invocable_v is only available after C++17, you can use a workaround like the following (not thoroughly tested, but I feel good about it):

template<typename F, typename ArgsTuple, typename Enable = void>
struct my_is_invocable_impl : std::false_type {};

template<typename F, typename... Args>
struct my_is_invocable_impl<
    F,
    std::tuple<Args...>,
    decltype(std::declval<F>()(std::declval<Args>()...))
> : std::true_type {};

template<typename T, typename... Args>
constexpr bool my_is_invocable = my_is_invocable_impl<T, std::tuple<Args...>>::value;

// Some test cases
static_assert(my_is_invocable<void(*)(int, double), int, double>, "Oops");
static_assert(my_is_invocable<void(*)(void*), void*>, "Oops");
static_assert(my_is_invocable<void(*)()>, "Oops");
static_assert(!my_is_invocable<void(*)(int, double)>, "Oops");
static_assert(!my_is_invocable<void(*)(void*)>, "Oops");

This can be used as a drop-in replacement for std::is_invocable_v in the above solution. See the demo for the full example, including generic lambdas.

Live Demo for C++14

Upvotes: 2

max66
max66

Reputation: 66210

Sorry but... a template function

template <int N>
void f1(Value<N>& item)
{
    for (auto& i : item.value) {
        i = 123;
    }
}

isn't an object but a set of object; so you can't pass it to a another function as argument

exec.do_exec(vec, f1);

The same for f2.

But you can wrap it inside an object (a lambda function is syntactic sugar for this type of solution)

struct foo_1
 {
   template <int N>
   void operator() (Value<N>& item)
    {
      for (auto& i : item.value)
         i = 123;
    }
 };

struct foo_2
 {
   template <int N>
   void operator() (Value<N>& item, int d)
    {
      for (auto& i : item.value)
         i = d;
    }
 };

so you can send the full set of function as follows

int main()
{
    Executor exec;
    std::vector<Value<3>> vec(10);

    foo_1 f1;
    foo_2 f2;

    exec.do_exec(vec, f1);
    exec.do_exec(vec, f2);
}

This should works (but not your commented Executor example in compiler explorer because the first do_exec() isn't SFINAE enabled/disabled)

The following is a modified version of you original compiler explorer example with a couple of calls to do_exec() with generic lambdas.

#include <functional>
#include <iostream>
#include <numeric>
#include <type_traits>
#include <vector>
#include <array>


template <int Num>
struct Value {
  std::array<int, Num> value;
};


template <typename Fn, typename T, typename = void>
struct HasIndex : std::false_type {};   

template <typename Fn, typename T>
struct HasIndex<Fn, T, std::void_t<std::invoke_result_t<Fn, T&, int>>> : std::true_type {};

struct Executor {
    template <int N, typename Fn,
              std::enable_if_t<!HasIndex<Fn, Value<N>>::value, int> = 1>
    void do_exec(std::vector<Value<N>>& n, Fn func) {
        for (auto& item : n)
            func(item);
    }

    template <int N, typename Fn,
              std::enable_if_t<HasIndex<Fn, Value<N>>::value, int> = 1>
    void do_exec(std::vector<Value<N>>& n, Fn func) {
        for (auto i = 0u; i != n.size(); i++)
            func(n[i], int(i));
    }
}; 

struct foo_1
 {
   template <int N>
   void operator() (Value<N>& item)
    {
      for (auto& i : item.value)
         i = 123;
    }
 };

struct foo_2
 {
   template <int N>
   void operator() (Value<N>& item, int d)
    {
      for (auto& i : item.value)
         i = d;
    }
 };

template <int N>
void read(const Value<N>& item)
{
    for (auto& i : item.value) {
        std::cout << i << " ";
    }
}


int main()
{
    Executor exec;
    std::vector<Value<3>> vec(10);

    foo_1 f1;
    foo_2 f2;

    exec.do_exec(vec, f1);
    exec.do_exec(vec, f2);
    exec.do_exec(vec, [](auto & item)
     { for ( auto & i : item.value ) std::cout << i << std::endl; });
    exec.do_exec(vec, [](auto & item, int d)
     { for (auto& i : item.value) i = d; });
}

Upvotes: 2

Related Questions