C++17 关键新特性介绍及代码讲解 (6) — fold expression

C++17 关键新特性介绍及代码讲解 (6) — fold expression

一句话概括:

fold expression 是将 parameter pack 进行递归展开、并通过二元操作符 (binary operator) 进行运算的一种表达式。

parameter pack 包括: template parameter pack、 function parameter pack, 以下面代码中的函数声明为例,
typename ... Args为 template parameter pack, Args... args为 function parameter pack:


template <typename ... Args>
constexpr auto unary_right_sub(Args... args);

fold expression 的详细定义参见 [fold expression]

parameter pack 的详细定义参见 [parameter pack]

在 C++17 中,总共定义有四种类型的 fold expression, 我们结合例子来看一下:


1. unary right fold

语法:

( E op … )

扩展为

( E1 op ( … op ( EN-1 op EN ) ) )

E 的定义 :一个包含未展开的 parameter pack 的表达式;如上文定义的,parameter pack 可以是 template parameter pack 或者
function parameter pack;

op: 即二元操作符,且可以被重载,包括:

+  -  *  /  %  ^  &  |  ~  =  <  >  <<  >> +=  -=  *=  /=  %=  ^=  &=  |=  <<=  >>= ==  !=  <=  >=  &&  ||  ,  .*  ->*         

N:parameter pack 中包含的元素个数;

在后文将要介绍到的另外三种类型 fold expression 的定义中,E 和 op 的定义与这里相同,不再赘述;

我们通过下面这段代码,来看看 unary right fold 是怎么展开的:


/*** 1. unary right fold ***/
template <typename ... Args>
constexpr auto unary_right_sub(Args... args){
    return (args - ...);  // unary right fold expression.
}

/*** test unary right fold ***/
constexpr auto u_r_sub = unary_right_sub(1, 2, 3, 4);
static_assert(u_r_sub == (1 - (2 - (3 - 4))));

上面代码中,unary_right_sub函数中的 unary right fold 表达式是:(args - ...)

即:

E 是 args,即仅包含 function parameter pack 的一个表达式;

op-操作符;

unary_right_sub(1, 2, 3, 4)最终展开为 (1 - (2 - (3 - 4))))

对比一下,在cpp17之前,如果要实现上述的 unary_right_sub 函数功能,我们需要用递归函数的方法展开:


// before C++17, we must use the recursive implementation for variadic template.
// additional stop case function must be defined here.
template <typename T1, typename T2>
constexpr auto recursive_right_sub(T1 arg1, T2 arg2){
    return (arg1 - arg2);
}
template <typename T1, typename ... Args>
constexpr auto recursive_right_sub(T1 arg1, Args... args) {
    return (arg1 - recursive_right_sub(args...));
}

// test of recursive implementation. 
constexpr auto u_r_sub = unary_right_sub(1, 2, 3, 4);
constexpr auto r_r_sub = recursive_right_sub(1, 2, 3, 4);
static_assert(u_r_sub == r_r_sub);

可以看到,fold expression 这种写法的好处是,大大简化了 variadic template 的实现,大多数情况下,不再需要用递归函数的方式去实现
variadic template。


2. unary left fold

语法:

( … op E )

扩展为

( ( ( E1 op E2 ) op … ) op EN )

代码举例:


/*** 2. unary left fold ***/
template <typename ... Args>
constexpr int unary_left_sub(Args... args){
    return (... - args); // unary left fold expression.
}

/*** test unary left fold ***/
constexpr auto u_l = unary_left_sub(1, 2, 3, 4);
static_assert(u_l == ((( 1 - 2 ) - 3 ) - 4));

即,unary_left_sub(1, 2, 3, 4)通过 (... - args) 扩展为了 ((( 1 - 2 ) - 3 ) - 4)


3. binary right fold

语法:

( E opop I)

扩展为

( E1 op ( … op ( EN-1 op ( EN op Init ) ) ) )

Init 的定义 : 一个没有包含未展开的 parameter pack 的表达式;

代码举例:假设我们有一个已经包含初始元素的 tuple 数据结构,接着我们需要实现一个叫做 binary_right_cat_tuple的函数,
该函数接受任意类型、任意个数的参数,并且将这些参数,按顺序添加到初始的 tuple 中,且位置位于 tuple 中所有初始元素的序列前。

为了实现方便,我们先重载一下 >>二元运算符:重载后的功能是将一个任意类型的参数,同一个已知的 tuple 数据结构,
"拼接"为一个全新的 tuple:


// overload '>>' operator.
template <typename ElementType,  typename TupleType>
constexpr auto operator>>(ElementType&& e, TupleType&& t){
    return std::tuple_cat(std::make_tuple(e), t);
}

接着,我们再定义 binary_right_cat_tuple 函数:


/*** 3. binary right fold ***/
template <typename TupleType, typename ... Args>
constexpr auto binary_right_cat_tuple(const TupleType& init_tuple, Args ... args){
    return ( args >> ... >> init_tuple );
}
/*** test binary right fold ***/
using namespace  std::literals::string_literals;
const auto init_tuple = std::make_tuple("stay calm"s);
const auto concat_tuple = binary_right_cat_tuple(init_tuple, "Jack"s, M_PI,  "hello world"s, 1988);
// just for compare.
const auto make_concat_tuple = std::make_tuple("Jack"s, M_PI,  "hello world"s, 1988, "stay calm"s);
static_assert(std::is_same_v<decltype(concat_tuple), decltype(make_concat_tuple)>);

上面代码中,binary_right_cat_tuple函数中的 binary right fold 表达式即为:

( args >> ... >> init_tuple )

我们对照上文中的语法定义:

E 是 args,即仅包含 function parameter pack 的一个表达式;

op 是重载过的 >>操作符;

Init 是 init_tuple,即初始的 tuple 数据结构;

即,

binary_right_cat_tuple(init_tuple, "Jack"s, M_PI, "hello world"s, 1988)

最终展开为

( "Jack"s >> ( M_PI >> ( "hello world"s >> ( 1988 >> init_tuple ) ) ) )

最后,我们得到的 concat_tuple 即包含 "Jack"s, M_PI, "hello world"s, 1988, "stay calm"s 所有元素。

下面我们讲 binary left fold 的代码例子里,会把concat_tuple 打印出来。


4. binary left fold

语法:

( I opop E )

扩展为

( ( ( ( I op E1) op E2 ) op … ) op EN )

代码举例 :我们想要实现一个打印函数,可以接受任意类型的 tuple 参数,并将 tuple 的每个元素逐行打印出来。

为了实现方便,我们先定义 to_string_helper函数:该函数可以将非 string 类型的参数转换为 string,而对于 string 类型的
参数则不做转换,这个与 fold expression 无关,大家可以略过:


// helper functions.using SFINAE.
template <typename T, std::enable_if_t<!std::is_same_v<T, std::string>, bool> = true >
auto to_string_helper(T t){
    return std::to_string(t);
}
template <typename T, std::enable_if_t<std::is_same_v<T, std::string>, bool> = true >
auto to_string_helper(T t){
    return t;
}

接着,我们定义用 binary left fold 实现的打印函数 binary_left_print_tuple


/*** 4. binary left fold ***/
// print tuples.
template <typename TupleType, size_t ...Idx>
constexpr void binary_left_print_tuple(TupleType& t, std::index_sequence<Idx...>){
    ( std::cout << ... << (to_string_helper(std::get<Idx>(t)) + "\n" ) );
}

using namespace  std::literals::string_literals;
const auto init_tuple = std::make_tuple("stay calm"s);
const auto concat_tuple = binary_right_cat_tuple(init_tuple, "Jack"s, M_PI,  "hello world"s, 1988);

/*** test binary left fold ***/
constexpr auto concat_tuple_size = std::tuple_size_v<decltype(concat_tuple)>;
std::cout << "-- the elements of concat_tuple: " << std::endl;
binary_left_print_tuple(concat_tuple, std::make_integer_sequence<size_t, concat_tuple_size>());

上面代码中,binary_left_print_tuple函数中的 binary left fold 表达式即为:

( std::cout << ... << (to_string_helper(std::get(t)) + "\n" ) )

我们对照上文中的语法定义:

E 是 (to_string_helper(std::get(t)) + "\n" ),这里 Idx 是 non-type template parameter pack;表达式 E
的功能是将 tuple 数据结构 t中的元素按照 Idx 的值索引取出,再调用 to_string_helper函数转换为 string 类型,
最后添加换行符 "\n"; 这里我们看到, E 可以包含各种函数调用;

op 是标准定义的 << 操作符;

Init 是 std::cout ,即系统标准输出流;

即:

binary_left_print_tuple(concat_tuple, std::make_integer_sequence())

最终展开为

( ( ( ( ( std::cout << "Jack\n"s ) << "3.141593\n" ) << "hello world\n"s ) << "1988\n" ) << "stay calm\n" )

代码运行得到的打印结果如下:


-- the elements of concat_tuple: 
Jack
3.141593
hello world
1988
stay calm

完整 demo代码参见 gitee.com 上的公开代码仓库 : [fold expression]


扩展阅读:

fold 的概念来自于 functional programming,参见这篇文章 [Folding things with Functional Programming]

你可能感兴趣的:(c++,c++,开发语言)