【问题标题】:C++ Compile time specific functions with variadic templatesC++ 使用可变参数模板编译时间特定的函数
【发布时间】:2016-01-07 19:48:22
【问题描述】:

我目前正在开发一个函数抽象库。我想将多参数函数抽象为作为索引的单参数函数。 (可选)我想传递一个谓词对象来确定它是否应该运行代码。

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 ...);
       ^~~~~~~~~

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

【问题讨论】:

  • 您可能想要更改您的标题。对 C++ 说“可变参数”会让人心脏病发作
  • “可变参数模板”听起来更好吗? ;)
  • 请注意,您有一个错误 - 您按价值获取 args,但您通过引用捕获它们

标签: c++ templates c++11 lambda


【解决方案1】:

让我们看看你的签名:

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. 提出的更好的解决方案。在 cmets 中只是引入一个空标签类型:

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_fns 时,你有悬空引用:

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

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

【讨论】:

  • 我明白了。所以你的意思是更明确的谓词类型应该可以解决问题?我尝试使用类型 'std::function 但现在编译器说:“在函数模板专业化的实例化中 'make_fn' ... 候选函数不可行:需要 2 个参数,但提供了 3 个。
  • @dommynik 我没说要使用std::function。即使您传递谓词,这也会导致(2) 成为首选,因为它具有更好的转换顺序。
  • 这是正确的实现方式吗? template&lt;typename Fn, typename Predicate, typename ... Args, typename = decltype(std::declval&lt;Predicate&gt;()(0u))&gt; auto make_fn(const Fn &amp;fn, const Predicate &amp;predicate, Args &amp;&amp; ... args) { return [&amp;](const unsigned long idx) { if(predicate(idx)) fn(idx, std::forward&lt;Args&gt;(args) ...); }; }
  • SFINAE 非常脆弱。我会建议使用更强的不同名称(或使用标签类型 - 比如struct predicate_arg_t {};)。此外,捕获需要按价值计算;转发是不够的 - 创建悬空引用太容易了(例如,如果您存储引用,FnType new_fn2 = make_fn(fn, 2.0); 将始终悬空。)
  • @T.C.使用标签类型是什么意思?使用按值捕获我得到了这个(并且它工作稳定):template&lt;typename Fn, typename Predicate, typename ... Args, typename = decltype(std::declval&lt;Predicate&gt;()(0u))&gt; FnType make_fn(Fn fn, Predicate predicate, Args const &amp; ... args) { return [=](const unsigned long idx) { if(predicate(idx)) fn(idx, args ...); }; } - 通过使用std::forward 我得到了分段错误。
【解决方案2】:

将你的上层函数更改为

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&amp; ... args

【讨论】:

  • 如果我们想要一个谓词但没有参数怎么办?
  • @Barry: 公平点 ;-) ...所以检查谓词中的函数类型。
猜你喜欢
  • 1970-01-01
  • 2018-03-25
  • 1970-01-01
  • 1970-01-01
  • 2021-10-01
  • 2011-06-29
  • 2015-12-21
  • 2011-11-03
  • 1970-01-01
相关资源
最近更新 更多