【问题标题】:How to SFINAE using the function signature of a template function如何使用模板函数的函数签名进行 SFINAE
【发布时间】:2020-07-13 21:02:38
【问题描述】:

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

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 lambdas 调用它。

我尝试用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);
    }
}; 

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

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

https://godbolt.org/z/W7z3Mv

【问题讨论】:

  • 如果您的 lambda 是无捕获且不是通用的,您可以将其转换为函数指针。
  • @max66:不幸的是,执行器采用的 lambdas 总是通用的。

标签: c++ c++14 template-meta-programming sfinae


【解决方案1】:

修复相当简单。首先,我将使用类型特征库中的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);
}

对我来说,这会因为一个非常通用的模板参数推导/替换失败而失败。要完成这项工作,您需要使用 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

【讨论】:

  • 传入f1确实很简单。尝试使用通用 lambda,您的解决方案将无法正常工作。你基本上输入了我的第二个解决方案并用 f1 代替
  • @apramc 它确实有效(尽管使用 C++17,我刚刚看到你的注释)。我在this demo 中添加了一个通用 lambda。我会尝试想出一些对 C++14 友好的东西
  • C++17 让生活更轻松,当然。
【解决方案2】:

对不起,但是...一个模板函数

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)

以下是原始编译器资源管理器示例的修改版本,其中使用通用 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; });
}

【讨论】:

  • 第一个 do_exec() 实际上是在检查它是否没有索引,我在尝试不同的东西,然后我改变了它。我更新了godbolt链接。如果你看到了,我在问题的正文中有一个更新的 sfiane 方法。
  • 另一个问题是,它适用于 GENERIC lamdas 吗?如果我使用通用 lambda 调用它,我可以指定调用哪个函数吗?
  • @apramc - 但是您的原始 Godbolt 代码几乎可以使用通用 lambda(将 SFINAE 部分添加到第一个 do_exec() 版本)。答案改进了添加带有几个 lambda 的代码的修改版本。
  • 是的,它只是不适用于像函数指针方法那样的通用函数,我希望有一个涵盖所有情况的解决方案,但似乎不可能。谢谢
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多