【问题标题】:Functional programming using parameter packs in C++使用 C++ 中的参数包进行函数式编程
【发布时间】:2015-12-10 16:35:44
【问题描述】:

这是对我遇到的另一个问题的简化,但它本身就很好。这个想法是在Scheme中实现类似于mapapply的功能原语。

回顾一下:在Scheme中,给定一个函数f,那么(apply f '(1 2 3))等价于(f 1 2 3)(map f '(1 2 3))等价于((f 1) (f 2) (f 3))

实现apply 是一件容易的事,还有很多其他问题说明了这是如何完成的:

template <class Func, class... Args, std::size_t... Ixs>
auto apply_helper(Func&& func, const tuple<Args...>& args,
                  index_sequence<Ixs...>)
    -> decltype(func(get<Ixs>(args)...))
{
  return forward<Func>(func)(get<Ixs>(forward<const tuple<Args...>&>(args))...);
}

template <class Func, class... Args,
          class Ixs = make_index_sequence<sizeof...(Args)>>
auto apply(Func&& func, const tuple<Args...>& args)
    -> decltype(apply_helper(func, args, Ixs()))
{
  return apply_helper(forward<Func>(func),
                      forward<const tuple<Args...>&>(args), Ixs());
}

void print3(int x, const char* s, float f) {
  cout << x << "," << s << "," << f << endl;
}

int main() {
  auto args = make_tuple(2, "Hello", 3.5);
  apply(print3, args);
}

现在实现map,这有点棘手。我们希望这样的东西能够工作,所以这是目标(这里使用mapcar 以避免与std::map 冲突):

template <class Type>
bool print1(Type&& obj) {
  cout << obj;
  return true;
}

int main() {
  auto args = make_tuple(2, "Hello", 3.5);
  mapcar(print1, args);
}

传递print1 函数的其他替代方法也可以。

所以,如果我们对函数进行硬编码,下面的代码就可以正常工作:

template <class... Args, std::size_t... Ixs>
auto mapcar_helper(const tuple<Args...>& args,
                   index_sequence<Ixs...>)
    -> decltype(make_tuple(print1(get<Ixs>(args))...))
{
  return make_tuple(print1(get<Ixs>(forward<const tuple<Args...>&>(args)))...);
}

template <class... Args,
          class Ixs = make_index_sequence<sizeof...(Args)>>
auto mapcar(const tuple<Args...>& args)
    -> decltype(mapcar_helper(args, Ixs()))
{
  return mapcar_helper(forward<const tuple<Args...>&>(args), Ixs());
}

问题是我们如何推广此代码以接受任意名称作​​为输入并让它解析模板内的名称查找?仅仅添加一个模板参数是行不通的,因为它无法解决此时的函数重载。

我们想让上面的mapcar调用相当于代码:

make_tuple(print1(2), print1("Hello"), print1(3.5));

更新:最初的挑战之一是让它与 C++11 编译器一起工作,部分原因是我使用的是 GCC 4.8,但也因为我想研究如何做到这一点。基于 cmets,下面是一个示例,说明如何在没有多态 lambda(需要 C++ 14 编译器支持)的帮助下完成

这并不像我想的那么简单,C++ 14 的特性会让它变得如此简单,但至少可以在给用户带来一点不便的情况下得到支持。

template <class Func, class... Args, std::size_t... Ixs>
auto mapcar_helper(Func&& func, const tuple<Args...>& args,
                   index_sequence<Ixs...>)
    -> decltype(make_tuple(func(get<Ixs>(args))...))
{
  return make_tuple(func(get<Ixs>(args))...);
}

template <class Func, class... Args,
          class Ixs = make_index_sequence<sizeof...(Args)>>
auto mapcar(Func&& func, const tuple<Args...>& args)
   -> decltype(mapcar_helper(func, args, Ixs())
{
  return mapcar_helper(forward<Func>(func), forward<decltype(args)>(args), Ixs());
}

为了能够传递模板“函数”,我们需要将它包装在一个对象中:

struct print1 {
  template <class Type> const Type& operator()(Type&& obj) {
    std::cout << obj << " ";
    return obj;
  }
};

现在可以将其传递给函数,类型查找将在参数包扩展点完成:

   mapcar(print1(), make_tuple(2, "Hello", 3.5));

【问题讨论】:

  • 在 C++14 中,您不再需要指定函数的返回类型。您可以简单地将auto 指定为返回类型,不再需要-&gt; decltype(...)
  • 你必须通过Functor,你不能通过generic函数。
  • 顺便说一句,这是一个很好的问题!
  • stackoverflow.com/users/2684539/jarod42 是的,这是真的。在这里研究 C++ 11 和 C++ 14 可以处理的内容。

标签: c++ templates c++11 c++14 template-meta-programming


【解决方案1】:
template <typename F, class... Args, std::size_t... Ixs>
auto mapcar_helper(F f, const tuple<Args...>& args,
                   index_sequence<Ixs...>)
    -> decltype(make_tuple(f(get<Ixs>(args))...))
{
  return make_tuple(f(get<Ixs>(args))...);
}

template <typename F, class... Args,
          class Ixs = make_index_sequence<sizeof...(Args)>>
auto mapcar(F f, const tuple<Args...>& args)
    -> decltype(mapcar_helper(move(f), args, Ixs()))
{
  return mapcar_helper(move(f), args, Ixs());
}

然后你做:

mapcar([](auto&& obj) { return print1(std::forward<decltype(obj)>(obj)); }, args);

也许我没听懂这个问题。您需要将 print1 包装在 lambda 中,否则它会模棱两可;你想传递print1 的哪个实例化?


如果你没有宏观恐惧症,你可以使用宏来让它更优雅:

#define LIFT(F) ([&](auto&&... args) -> decltype(auto) { \
    return F(::std::forward<decltype(args)>(args)...);  \
})

那么你可以使用mapcar(LIFT(print1), args)


这就是我编写自己的map 函数的方式:

template<typename F, class Tuple, std::size_t... Is>
auto map(Tuple&& tuple, F f, std::index_sequence<Is...>)
{
    using std::get;
    return std::tuple<decltype(f(get<Is>(std::forward<Tuple>(tuple))))...>{
        f(get<Is>(std::forward<Tuple>(tuple)))...
    };
}

template<typename F, class Tuple>
auto map(Tuple&& tuple, F f)
{
    using tuple_type = std::remove_reference_t<Tuple>;
    std::make_index_sequence<std::tuple_size<tuple_type>::value> seq;
    return (map)(std::forward<Tuple>(tuple), std::move(f), seq);
}

【讨论】:

  • 嗯...问题本质上是是否可以将名称解析移动到模板函数内部,在包扩展时,而不是在调用之前。如果您尝试直接通过上面的print1,您得到的编译错误是它无法解析实例化,但将名称直接放在那里是有效的,所以我想知道它是否是该语言的一个缺点可以解决。上面的代码适用于 C++ 14,因为它需要多态 lambda。有没有办法在 C++ 11 中做到这一点?
  • @MatsKindahl:你可以用旧的方式编写你的函子:struct printer { template &lt;typename T&gt; void operator()(T&amp;&amp; obj) const {/*..*/} };
  • 你也可以只传递 print1 而不是 lambda,我一直这样做。在这里不行吗?
  • @johnbakers 通常,您不应传递要推断的显式模板参数。您应该让编译器完成这项工作。
  • @johnbakers 好吧,你可能不知道类型,所以它不会帮助你。这个想法是研究如何让编译器在包展开时实际进行推导。
【解决方案2】:

我错过了什么?

#include <iostream>
#include <string>


template<class F, class...Args>
void map(F&& f, Args&&...args)
{
    using expander = int[];
    (void) expander { 0, ((void) f(args), 0)... };
}

auto main() -> int
{
    using namespace std;

    map([](const auto& x) { cout << x << endl; }, 1, "hello"s, 4.3);

    return 0;
}

预期输出:

1
hello
4.3

请注意,在 c++17 中,map() 函数变得更讨人喜欢:

template<class F, class...Args>
void map(F&& f, Args&&...args)
{
    (f(args), ...);
}

如果您的下一个问题是“为什么要使用括号?”。答案是因为 折叠表达式 仅在表达式的上下文中进行评估。 f(arg1), f(arg2); 是一个声明。

参考:http://en.cppreference.com/w/cpp/language/fold

【讨论】:

  • 这两行expander 很有趣;我不太清楚他们在做什么
  • 它只是在创建一个整数数组。每个元素都初始化为执行 f(args) 的表达式 (f(args),0) 的结果,丢弃结果然后返回 0(这只是工作中的逗号运算符)。然后,按顺序评估所有函数调用后,临时数组被优化器丢弃,因为没有使用任何值。
  • 第一个 0 的原因是在 Args... 为空的情况下。在 C++ 中不允许有 0 长度的数组(你在 C 中)
  • 至于为什么逗号操作符会丢掉结果返回零,相关问题:stackoverflow.com/questions/54142/…
  • @MatsKindahl 在这种情况下将 lambda 替换为带有模板化 operator()(T&) 的函数对象
猜你喜欢
  • 2015-05-30
  • 2018-05-04
  • 2014-10-02
  • 1970-01-01
  • 1970-01-01
  • 2021-08-10
  • 2010-10-28
  • 2020-01-31
  • 1970-01-01
相关资源
最近更新 更多