使用C++中的参数包进行函数式编程

Functional programming using parameter packs in C++

本文关键字:函数 编程 包进行 参数 C++ 使用      更新时间:2023-10-16

这是对我遇到的另一个问题的简化,但它本身就很好。其思想是在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,但也因为我想研究如何做到这一点。根据评论,下面是一个如何在没有多态lambdas(需要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));
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);
}

我错过了什么?

#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