C++17折叠表达式
2021年9月18日
字数统计:1579
C++17中对可变参数模板的参数包进行了一项改进,即使用折叠表达式(Fold Expression)来简化递归式的“函数调用”,简化了语法。
在之前的文章C++可变参数模板 和C++17 constexpr if和constexpr lambda 中都有提到过参数包(parameter pack),其中利用参数包实现了一个接收任意数量的参数的variadicPrint打印函数,实际上还可以通过折叠表达式进一步简化。
使用折叠表达式的前提是使用受支持的32个操作符:+ - * / % ^ & | = < > << >> += -= *= /= %= ^= &= |= <<= >>= == != <= >= && || , .* ->*。
C++17的折叠表达式根据标识符的位置分为左折叠和右折叠,根据操作的对象数量分为一元折叠和二元折叠。
只有三个运算符允许参数包为空:&& || 和 ,,其中&& 为true,|| 为false,, 为 void()。
一元折叠
假设表达式是E,操作符是op,E包含标识符(参数包):
一元左折叠:(... op E) 展开为 (E1 op (... op (EN-1 op EN )))
一元右折叠:(E op ...) 展开为 (((E1 op E2 ) op ...) op EN )
折叠表达式其实就是使用折叠标记 ... 和参数包 args 将参数展开的的语法糖,任何折叠表达式都包含折叠标记,标识符和操作符三部分。
最简单的折叠表达式的实例是求和函数:
template < typename ... Ts >
auto sumL ( Ts ... ts )
{
return ( ts + ...); // 右折叠
}
template < typename ... Ts >
auto sumR ( Ts ... ts )
{
return (... + ts ); // 左折叠
}
当调用 sum(1, 2, 3, 4, 5) 时,右折叠会沿右侧不断将参数包展开,变为 1 + (2 + (3 + (4 + 5)))(括号只是为了说明展开方向,真实结果不会添加括号),这其中经历了3次展开,第一次展开为 1 + (2 + ts),然后继续进行第二次展开为 1 + (2 + (3 + ts))。左折叠的展开方向与之相反。
还可以有下面这个稍微复杂点的例子:
template < class ... T >
void variadicPrint ( T ... t )
{
(( std :: cout << t ), ...) << std :: endl ;
}
template < class ... T >
void variadicPrint ( T ... t )
{
(..., ( std :: cout << t << std :: endl )); // 左右折叠都可
}
其中 (std::cout << t)(或者 (std::cout << t << std::endl))是包含参数包的表达式,编译阶段使用逗号运算符连接展开的表达式,复制 (std::cout << t),并将参数包t替换为实际参数。
对于大部分不需要考虑结合性的情况,左折叠和右折叠没有区别;其他一些情况则需要考虑结合性,例如 std::string 的字符串字面值赋值,必须使用右折叠,因为字符串字面值是右值,不能做加运算,此外除法和减法也类似。
由于模板是在编译期进行推导,所以其实不必通过函数的参数传递参数,允许直接将参数直接传递给模板:
template < auto ... T >
void variadicPrint ()
{
(( std :: cout << T ), ...) << std :: endl ;
}
int main ()
{
variadicPrint < 1 , 2 , 3 > ();
}
不过这也存在着非常明显的缺陷:模板参数类型必须为常量,所以必须为constexpr类型的变量才可做为模板参数,这极大的限制了这个函数的用途,因为用户自定义类基本都不是constexpr的。
C++17添加了 std::string_view 来构造一个不需要内存分配的“字符串”,以及C++20添加了 std::string 的constexpr构造,因此可以通过此方法通过模板参数输出一个字符串:
#include <iostream>
#include <string_view>
using namespace std :: literals ;
template < const auto & ... T >
void variadicPrint () {
(( std :: cout << T ), ...) << std :: endl ;
}
constexpr auto a = "aaa" sv ; // 注意,a必须是具有静态储存期的常量表达式
int main () { variadicPrint < a > (); }
一元折叠技巧
通过上面以及之前的代码可以实现一个打印函数,这个函数每打印一次就可以换一次行,也可以打印多次最后再换行,那么可不可以像参数列表一样只在前n - 1次打印时输出逗号,最后一次不输出呢?答案肯定是可以的,有四种方法可以实现这种效果:
使用运行期迭代:
template < class ... T >
void variadicPrint ( T ... t )
{
constexpr last = sizeof ...( t ) - 1 ;
int i = 0 ;
(( i < last ? ( std :: cout << t << ", " ) : ( std :: cout << t << std :: endl ), ++ i ), ...);
}
使用if constexpr:
template < typename T , typename ... Ts >
void variadicPrint ( T head , Ts ... tail )
{
std :: cout << head << ", " ;
if constexpr ( sizeof ...( tail ) > 0 )
variadicPrint ( tail ...);
std :: cout << std :: endl ;
}
使用lambda递归:
template < typename Head , typename ... T >
void variadicPrint ( const Head & head , const T & ... args ) {
std :: cout << first ;
auto wrapper = []( const auto & arg ){
std :: cout << ", " ;
return arg ;
};
( std :: cout << ... << wrapper ( args ));
}
使用lambda迭代:
template < class ... T >
void variadicPrint ()
{
constexpr last = sizeof ...( t ) - 1 ;
int i = 0 ;
auto wrapper = [ i ] < class Arg > ( Arg arg ) mutable // C++20
{
if ( last == i )
{
std :: cout << arg << endl ;
} else
{
std :: cout << arg << ", " ;
}
}
( wrapper ( t ), ...);
}
constexpr if的写法其实是效率最高也最直观的,因此一般推荐使用该方法。
二元折叠
二元左折叠:(I op ... op E) 展开为 ((((I op E1 ) op E2 ) op ...) op EN )
二元右折叠:(E op ... op I) 展开为 (E1 op (... op (EN-1 op (EN op I))))
虽然一元折叠已经足够好用,但是二元折叠仍然有其用武之地:
template < typename ... Ts >
int removeFrom ( int num , Ts ... args )
{
return ( num - ... - args ); //Binary left fold
// Note that a binary right fold cannot be used
// due to the lack of associativity of operator-
}
int result = removeFrom ( 1000 , 5 , 10 , 15 ); //'result' is 1000 - 5 - 10 - 15 = 970
参考
若无特殊声明,本人原创文章以
CC BY-SA 4.0许可协议
提供。
本站不欢迎非搜索引擎类,非个人学习类爬虫;严禁将文章直接爬取至其他站点。
若看到此条消息,说明你正在访问的网站可能是垃圾二手转载网站。
为了获得更好的浏览体验,请访问唯一原始网站:mysteriouspreserve[dot]com或blog[dot]bizwen[dot]com。