C++使用可变模板编译特定于时间的函数

C++ Compile time specific functions with variadic templates

本文关键字:于时间 时间 函数 编译 C++      更新时间:2023-10-16

我目前正在开发一个函数抽象库。我想把一个多参数函数抽象成一个单参数函数,它是一个索引。可选地,我想传递一个谓词对象,该对象决定它是否应该运行代码。

using FnType = std::function<void(const unsigned long)>;
template<typename Fn, typename Predicate, typename ... Args>
const FnType make_fn(const Fn &fn, const Predicate &predicate, Args ... args) {
    return [&](const unsigned long idx) {
        if(predicate(idx)) fn(idx, args ...);
    };
}
template<typename Fn, typename ... Args>
const FnType make_fn(const Fn &fn, Args ... args) {
    return [&](const unsigned long idx) {
        fn(idx, args ...);
    };
}

当使用谓词时,此代码非常有效。

std::vector<double> data(10);
auto fn = [&](unsigned idx, double d) { data[idx] *= d; };
auto pred = [](unsigned long idx) { return idx % 2 == 0; };
FnType new_fn = make_fn(fn, pred, 2.0);

但是当我尝试不传递谓词时

FnType new_fn2 = make_fn(fn, 2.0);

它返回以下编译时错误消息:

called object type 'double' is not a function or function pointer
    if(predicate(idx)) fn(idx, args ...);
       ^~~~~~~~~

有没有可能说编译器应该使用第二个不使用任何谓词的函数?

让我们看看您的签名:

template<typename Fn, typename Predicate, typename ... Args>
const FnType make_fn(const Fn&, const Predicate&, Args ...); // (1)
template<typename Fn, typename ... Args>
const FnType make_fn(const Fn&, Args ... ) // (2)

当我们与通话时

make_fn(fn, 2.0);

Predicate没有什么特别之处——它只是另一种类型。因此,当我们进行过载解析时,我们有两个完全匹配。我们解析变参数的方法是(1)(2)更专业——因为(1)至少需要2个参数,(2)至少需要1个参数,所以前者更具体。这就是为什么我们称前者为。编译器从签名中不知道Predicate是什么意思。

最简单的解决方案是简单地重命名一个或另一个函数-您真的需要重载它们吗?拥有make_fn()make_predicate_fn()

或者,我们可以使用SFINAE通过强制Predicate是可调用的来改变这一点:

template <typename Fn, typename Predicate, typename... Args,
          typename = decltype(std::declval<Predicate>()(0u))>
const FnType make_fn(const Fn&, const Predicate&, Args ...);

这将使(1)不适合您的呼叫,因此(2)将是首选。尽管这还有一个缺点,如果你不想以这种方式使用谓词,而是简单地使用一些第一个参数是谓词的函数,该怎么办?会发生错误的事情。

T.C.在评论中提出的一个更好的解决方案是只引入一个空标签类型:

struct predicate_tag_t { };
constexpr predicate_tag_t predicate_tag{}; 

以及标签的过载:

template <typename Fn, typename Predicate, typename... Args>
FnType make_fn(const Fn&, predicate_tag_t, const Predicate&, Args&&... );
template <typename Fn, typename... Args>
FnType make_fn(const Fn&, Args&&... );

这样你的例子就变成了:

FnType new_fn = make_fn(fn, predicate_tag, pred, 2.0);
FnType new_fn2 = make_fn(fn, 2.0);

当你有时,你在两个make_fn中都有悬空引用

const FnType make_fn(const Fn &fn, Args ... args) {
    return [&](const unsigned long idx) { ... }
}

您正在复制所有参数,然后保留对所有参数的引用。但一旦make_fn完成,所有的args都将超出范围。你需要复制它们——所以[=]

将上层函数更改为

template<typename Fn, typename Predicate, typename ... Args
       , typename = std::enable_if_t<sizeof ... (Args) != 0> >
const FnType make_fn(const Fn &fn, const Predicate &predicate, Args ... args) {
    return [=](const unsigned long idx) {
        if(predicate(idx)) fn(idx, args ...);
    };
}

这确保了参数包中至少有一个成员,然后在过载解决过程中选择第二个函数。

需要注意的更多事项:

  • 最好将返回类型设置为auto——您仍然可以将其配置为std::function,但如果您简单地调用它,则需要更少的开销。

  • 可能您不想按值传递Args ...中的通用参数,所以最好编写Args const& ... args