相关文章系列
深入理解可变参数(va_list、std::initializer_list和可变参数模版)
目录
1.介绍
2.应用
2.1.使用折叠表达式
2.2.支持的运算符
2.3.使用折叠处理类型
3.总结
1.介绍
折叠表达式是C++17新引进的语法特性。使用折叠表达式可以简化对C++11中引入的参数包的处理,从而在某些情况下避免使用递归。折叠表达式共有四种语法形式。分别为一元的左折叠和右折叠,以及二元的左折叠和右折叠。
1、一元右折叠(unary right fold)
( pack op ... )
一元右折叠(E op ...)展开之后变为 E1 op (... op (EN-1 op EN))
2、一元左折叠(unary left fold)
( ... op pack )
一元左折叠(... op E)展开之后变为 ((E1 op E2) op ...) op EN
3、二元右折叠(binary right fold)
( pack op ... op init )
二元右折叠(E op ... op I)展开之后变为 E1 op (... op (EN−1 op (EN op I)))
4、二元左折叠(binary left fold)
( init op ... op pack )
二元左折叠(I op ... op E)展开之后变为 (((I op E1) op E2) op ...) op EN
op代表运算符:下列 32 个二元运算符之一:+ - * / % ^ & | = < > << >> += -= *= /= %= ^= &= |= <<= >>= == != <= >= && || , .* ->*。在二元折叠中,两个运算符必须相同。
pack代表参数包:含有未展开的形参包且在顶层不含优先级低于转型(正式而言,是 转型表达式)的运算符的表达式。
init代表初始值:不含未展开的形参包且在顶层不含优先级低于转型(正式而言,是 转型表达式)的运算符的表达式注意开闭括号也是折叠表达式的一部分。
这里的括号是必需的。但是,圆括号和省略号(...)不必用空格分隔。
初始值在右边的为右折叠,展开之后从右边开始折叠。而初始值在左边的为左折叠,展开之后从左边开始折叠。
不指定初始值的为一元折叠表达式,而指定初始值的为二元折叠表达式。
例如:
template<typename... Args>
bool all(Args... args) { return (... && args); }
template<typename... Args>
bool any(Args... args) {return (... || args);}bool b = all(true, true, true, false);
// 在 all() 中,一元左折叠展开成
// return ((true && true) && true) && false;
// b 是 false
将一元折叠用于长度为零的包展开时,只能使用下列运算符:
1) 逻辑与(&&)。空包的值是 true
2) 逻辑或(||)。空包的值是 false
3) 逗号运算符(,)。空包的值是 void()
注意:如果用作初值或形参包 的表达式在顶层具有优先级低于转型的运算符,那么它可以加括号,如:
template<typename... Args>
int sum(Args&&... args)
{
// return (args + ... + 1 * 2); // 错误:优先级低于转型的运算符return (args + ... + (1 * 2)); // OK
}
2.应用
2.1.使用折叠表达式
下面的函数返回所有传递参数的和:
#include <iostream>
#include <string>//[1]
template<typename First>
First foldSum1(First&& value)
{ return value;
}//[2]
template<typename First, typename... Rest>
First foldSum1(First&& first, Rest&&... rest)
{ return first + foldSum1(std::forward<Rest>(rest)...);
}//[3]
template<typename... T>
auto foldSum2(T... args)
{return (... + args); // ((arg1 + arg2) + arg3) ...
}//[4]
template<typename First, typename... Rest>
First foldSum3(First&& first, Rest&&... rest)
{ return (first + ... + rest);
}int main(void)
{auto i1 = foldSum1(58, 25, 128, -10); //201auto s1 = foldSum1(std::string("abcdefg "), std::string("1234567890 "), std::string("!"));//"abcdefg 1234567890 !"auto i2 = foldSum2(58, 25, 128, -10); //201auto s2 = foldSum2(std::string("abcdefg "), std::string("1234567890 "), std::string("!"));//"abcdefg 1234567890 !"auto i3 = foldSum3(58, 25, 128, -10); //201auto s3 = foldSum3(std::string("abcdefg "), std::string("1234567890 "), std::string("!"));//"abcdefg 1234567890 !"return 0;
}
1)在C++17之前,求和函数foldSum1的实现必须分成两个部分。其中[1]部分的foldSum1函数用于处理一个参数的情况。[2]部分的foldSum1函数用于处理两个及以上参数的情况。当参数个数大于一个时,[2]部分的foldSum1函数将前两个参数相加,然后递归调用自身。当参数个数只有一个时,[1]部分的foldSum1函数将此参数返回,完成求和。foldSum1(58, 25, 128, -10) = 58+foldSum1(25, 128, -10) = 58+25+foldSum1(128, -10) = 58+25+128+foldSum1(-10) = 58+25+128-10 = 201。
2)而在C++17之后,由于有了折叠表达式这个新特性,求和函数foldSum1不再需要处理特殊情况,实现大为简化。对于foldSum2(58, 25, 128, -10) = (((58+25)+128) -10) = 201。
还请注意,折叠表达式参数的顺序可能不同,而且很重要:
(... + args)
的结果是
((arg1 + arg2) + arg3) ...
也可以如:
(args + ...)
其结果是
(arg1 + (arg2 + arg3)) ...
上面foldSum2定义的函数不允许在添加值时传递空参数包,像下面调用会出现错误:
于是可改为:
//[3]
template<typename... T>
auto foldSum2(T... args)
{return (0 + ... + args); // ((arg1 + arg2) + arg3) ...
}
从概念上讲,我们添加0作为第一个操作数还是最后一个操作数并不重要:
//[3]
template<typename... T>
auto foldSum2(T... args)
{return (args + ... + 0); // ((arg1 + arg2) + arg3) ...
}
但是对于一元折叠表达式求值顺序很重要。对于二元折叠表达式也应该优选左折叠表达式:
(val + ... + args); // preferred syntax for binary fold expressions
2.2.支持的运算符
在C++中,除了以下二元运算符,所有的二元操作符都可以使用折叠表达式。如下所示:. 、 ->、 []。叠表达式可以使用逗号运算符,这样就可以在一行调用多个函数,如:
#include <iostream>
using namespace std;template<typename... Ts>
void printAll(Ts&&... mXs)
{(cout << ... << mXs) << endl;
}template<typename TF, typename... Ts>
void forArgs(TF&& mFn, Ts&&... mXs)
{(mFn(mXs), ...);
}int main()
{printAll(78, 7811.0, "6789"); //7878116789printAll(); // 空行forArgs([](auto a){cout << a;}, 78, 7811.0, "6789"); //7878116789forArgs([](auto a){cout << a;}); // 空操作return 0;
}
1) printAll函数实现了对不特定多数值的打印输出。该函数的实现采用了二元左折叠。
printAll(78, 7811.0, "6789")
= (cout << ... << pack(78, 7811.0, "6789") << endl
= ((cout << 78) << 7811.0) << "6789" << endl
= 打印7878116789并换行
2) 当二元折叠表达式的参数包为空时,其计算结果为该二元折叠表达式中所预设的初始值。
printAll()
= (cout << ... << pack()) << endl
= cout << endl
= 空行
3)forArgs函数实现了依次使用多个参数调用某个单参数函数的功能。该函数的实现采用了一元右折叠。
forArgs([](auto a){cout << a;}, 78, 7811.0, "6789")
= ([](auto a){cout << a;}(pack(78, 7811.0, "6789")), ...)
= [](auto a){cout << a;}(78), ([](auto a){cout << a;}(7811.0), ([](auto a){cout << a;}("6789")))
= 打印7878116789
4)当使用逗号的一元折叠表达式中的参数包为空时,其计算结果为标准规定的缺省值void()。
forArgs([](auto a){cout << a;})
= ([](auto a){cout << a;}(pack()), ...)
= void()
上面是将折叠应用在函数中,下面将讨论将折叠使用在类中,作为类的基类进行调用
template<typename... T>
class MultiT : private T...
{
public:
void print()
{(..., T::print());
}
};
class CTest1 {
public:void print() { std::cout << "CTest1::print()"<<std::endl; }
};
class CTest2 {
public: void print() { std::cout << "CTest2::print()"<<std::endl; }
};
class CTest3 {
public: void print() { std::cout << "CTest3::print()"<<std::endl; }
};int main()
{MultiT<CTest1,CTest2,CTest2> myTest;myTest.print();//输出结果为:CTest1::print() CTest2::print() CTest3::print()return 0;
}
使用折叠表达式将其展开,以便为每个基类调用print。也就是说,折叠表达式的语句扩展为:
(CTest1::print() , CTest2::print()) , CTest3::print();
但是,请注意,由于逗号运算符的性质,我们使用左折叠运算符还是右折叠运算符并不重要。函数总是从左到右调用。
(T::print() , ...);
括号只对调用进行分组,以便第一个print()调用与其他两个print()调用的结果组合在一起,如下所示:
CTest1::print() , (CTest2::print() , CTest3::print());
但是因为逗号运算符的求值顺序总是从左到右仍然是第一个调用CTest1::print()发生在括号内的两个调用组(CTest2::print(), CTest3::print())之前,其中中间调用CTest2::print()仍然发生在右边调用CTest3::print()之前。然而,由于左折叠表达式与结果的计算顺序相匹配,所以当将左折叠表达式用于多个函数调用时,再次建议使用它们。
2.3.使用折叠处理类型
通过使用类型特征,可以判断类或者函数中传入的参数类型是否相同,如:
template<typename T1, typename... TN>
constexpr bool isHomogeneous(T1, TN...)
{return (std::is_same_v<T1, TN> && ...);
}
int main()
{std::cout<<boolalpha<<isHomogeneous(12,4434,true)<<std::endl; //输出:falsereturn 0;
}
上面函数调用isHomogeneous(12,4434,true),返回的表达式扩展为:
std::is_same_v<int, int> && std::is_same_v<int, bool>
结果为假,而对:
isHomogeneous("q24214", "", "c3252352", "!");
来说结果为真,因为所有传入的实参类型被推导为const char*(注意,实参类型会退化,因为调用实参是按值传递的)。
3.总结
折叠表达式是一个强大的工具,但也需要谨慎使用。它可以使代码更简洁、更易于阅读,但也可能会使代码更难以理解。在使用折表达式之前,确保你理解了它的工作原理,并考虑是否有其他更直观的方法可以达到相同的效果。
参考:折叠表达式(C++17 起) - cppreference.com