在 C++ 中使用参数包进行函数式编程
Functional programming using parameter packs in C++
这是对我遇到的另一个问题的简化,但它本身就很好。这个想法是在 Scheme 中实现类似于 map
和 apply
的功能原语。
回顾一下:在 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,但也因为我想调查怎么做。根据评论,下面是一个如何 在没有 多态 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());
}
为了能够传递模板 "function",我们需要将其包装在一个对象中:
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), ...);
}
如果你的下一个问题是 "why the brackets?"。答案是因为 折叠表达式 仅在表达式的上下文中计算。 f(arg1), f(arg2);
是一个声明。
这是对我遇到的另一个问题的简化,但它本身就很好。这个想法是在 Scheme 中实现类似于 map
和 apply
的功能原语。
回顾一下:在 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,但也因为我想调查怎么做。根据评论,下面是一个如何 在没有 多态 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());
}
为了能够传递模板 "function",我们需要将其包装在一个对象中:
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), ...);
}
如果你的下一个问题是 "why the brackets?"。答案是因为 折叠表达式 仅在表达式的上下文中计算。 f(arg1), f(arg2);
是一个声明。