如何使用模板函数的函数签名进行 SFINAE

How to SFINAE using the function signature of a template function

我有一个代码接受一个函数并根据函数签名执行它,如下所示:

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);
    }
};

当用户传入以下函数之一时,Executor 运行 匹配其签名的do_exec()

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);
}

我想扩展此代码,使其可以采用 lambda 函数,因为在实际代码中,几乎所有代理都会使用 GENERIC lambda 调用它。

我尝试用 std::function 替换仿函数,但它失败了,因为 lambda 不是 std::function 并且类型推导并没有真正发生。

然后我尝试取出两个模板参数和 SFINAE 中与签名不匹配的参数,如下所示:

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);
    }
}; 

这也不起作用,因为执行程序将采用的函数始终是模板函数 (GENERIC Lambda)。我不知道如何解决这个问题,感谢任何帮助。

c++14 解决方案(我知道 invoke_result 是 c++17)

https://godbolt.org/z/W7z3Mv

抱歉,但是...模板函数

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

不是一个对象而是一组对象;所以你不能将它作为参数传递给另一个函数

exec.do_exec(vec, f1);

f2也一样。

但您可以将其包装在一个对象中(lambda 函数是此类解决方案的语法糖)

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

这样就可以发全套功能如下

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);
}

这应该有效(但不是您在编译器资源管理器中评论的 Executor 示例,因为第一个 do_exec() 不是 SFINAE enabled/disabled)

以下是原始编译器资源管理器示例的修改版本,其中使用通用 lambda 对 do_exec() 进行了几次调用。

#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; });
}

修复相当简单。首先,我将使用类型特征库中的 std::is_invocable_v 来测试 SFINAE 机制中的兼容函数签名。换行符使模板签名可读,我发现:

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) {
    [...]
}

这允许对函数和通用 lambda 的非模板引用,但以下内容尚不可用:

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

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

对我来说,这失败了一个非常通用的模板参数 deduction/substitution 失败。要完成这项工作,您需要使用 N 的值专门化 f1,如:

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


更新 C++14 兼容性

由于 std::is_invocable_v 仅在 C++17 之后可用,您可以使用如下解决方法(未经过彻底测试,但我感觉很好):

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");

这可以用作上述解决方案中 std::is_invocable_v 的直接替代。请参阅演示以获取完整示例,包括通用 lambda。

Live Demo for C++14