折叠表达式
C++17 折叠表达式让我们能以更加简单的语法形式,更加轻松的进行形参包展开。
本节也将重新复习形参包的知识,希望各位不要忘记了。
语法
( 形参包 运算符 ... ) (1)
( ... 运算符 形参包 ) (2)
( 形参包 运算符 ... 运算符 初值 ) (3)
( 初值 运算符 ... 运算符 形参包 ) (4)
- 一元右折叠
- 一元左折叠
- 二元右折叠
- 二元左折叠
折叠表达式的实例化按以下方式展开成表达式 e:
- 一元右折叠
(E 运算符 ...)
成为(E1 运算符 (... 运算符 (EN-1 运算符 EN)))
- 一元左折叠
(... 运算符 E)
成为(((E1 运算符 E2) 运算符 ...) 运算符 EN)
- 二元右折叠
(E 运算符 ... 运算符 I)
成为(E1 运算符 (... 运算符 (EN−1 运算符 (EN 运算符 I))))
- 二元左折叠
(I 运算符 ... 运算符 E)
成为((((I 运算符 E1) 运算符 E2) 运算符 ...) 运算符 EN)
(其中 N 是包展开中的元素数量)
- 折叠表达式是左折叠还是右折叠,取决于
...
是在“形参包”的左边还是右边。
实现一个 print 函数
- 以下代码来自01函数模板.md 中的可变参数模板的示例
cpptemplate<typename...Args> void print(const Args&...args){ int _[]{ (std::cout << args << ' ' ,0)... }; } print("luse", 1, 1.2); // luse 1 1.2
运用折叠表达式,我们可以简化 print
,而不再需要创建愚蠢的数组对象 _
。
template<typename...Args>
void print(const Args&...args) {
((std::cout << args << ' '), ...);
}
print("luse", 1, 1.2); // luse 1 1.2
这显然是 (1)一元右折叠,我们一步一步分析:
(std::cout << args << ' ')
就是语法中指代的形参包(其实说的是含有形参包的运算符表达式)。那么 ,
逗号就是运算符,最后 ...
。然后最外层有括号 ()
符合语法。
函数模板实例化、折叠表达式展开,大概就是:
void print(const char(&args0)[5], const int& args1, const double& args2) {
(std::cout << args0 << ' '), ((std::cout << args1 << ' '), (std::cout << args2 << ' '));
}
我不建议各位数括号,死记这个规则,知道大概的意思就行,运用逗号运算符进行这个折叠表达式还是简单的,多用用就好。
详细展示语法
折叠表达式这四种语法形式我们都需要学习,并且明白其区别,我们用一个一个示例来展示,你自然会感觉到区别,毕竟运行结果不一样。
你可以学不懂或用不到这所有形式,但我总得教。
一元折叠
template<typename...Args>
void print(const Args&...args) {
(...,(std::cout << args << ' '));
}
print("luse", 1, 1.2); // luse 1 1.2
这个示例就是参考我们上面用折叠表达式实现的 print
,只不过一开始的是“一元右折叠”,而我们这个示例是“一元左折叠”。
如你所见,这个 print
不管使用左折叠还是右折叠,运行结果是一样的,这是为什么呢?
实例化展开后是这样:
void print(const char(&args0)[5], const int& args1, const double& args2) {
((std::cout << args0 << ' '), (std::cout << args1 << ' ')), (std::cout << args2 << ' ');
}
其实这个括号根本不影响什么,我们可以得出结论:“对于逗号运算符,一元左折叠和一元右折叠没有区别”。
我们用一个非类型模板参数的变量模板来展示在一些情况下左折叠和右折叠是会造成不同结果的:
template<int...I>
constexpr int v_right = (I - ...); // 一元右折叠
template<int...I>
constexpr int v_left = (... - I); // 一元左折叠
int main(){
std::cout << v_right<4, 5, 6> << '\n'; //(4-(5-6)) 5
std::cout << v_left<4, 5, 6> << '\n'; //((4-5)-6) -7
}
这个示例很好,那么简单总结一下:左折叠和右折叠是需要注意的,它们的效果可能不同。
其实按照以上示例效果 (4-(5-6))
((4-5)-6)
还可以总结一段简单的话:右折叠就是先算右边,左折叠就是先算左边。
我知道你肯定有疑问了:
前面不是说了:“对于逗号运算符,一元左折叠和一元右折叠没有区别”,为啥这里还会有谁先算这种说法?
有这个想法就代表思考了,我们来讲一下。
逗号表达式其实也是右折叠先算右边,左折叠先算左边,但是但是,请注意:“,
”,逗号不同于其他运算符;
比如 (expr1,(expr2,expr3))
和 ((expr1,expr2),expr3)
因为逗号表达式的特性,从左往右顺序执行,逗号运算符本身不需要做什么运算,那么这些括号根本不影响什么,但是如果换成其他的运算符,比如 -
,就不同了,(expr1-(expr2-expr3))
和 ((expr1-expr2)-expr3)
显然不同,也就是前面的:(4-(5-6))
((4-5)-6)
。
“先算”这个词,不是各位想象的那种,一定要先进行运算产生结果,也可能不会有什么运算,比如逗号表达式;这个先算其实指代的是折叠表达式语法给加的括号。但是这个表达式到底是怎么样的运算,是要根据运算符的。
到此,我们讲明白了为什么逗号表达式一元左右折叠效果一样,有些情况效果不一样;以及一元折叠的内容。
补充:各位要明白一件事情,我们说的“逗号特殊”,不是它真的特殊,对于折叠表达式规则而言,这些运算符都是一样的处理的,根据你自己运算符的行为,彼此之间毫无区别。我为什么要说“逗号特殊”?这是一个抽象的指代,指代的是大家看到可能不懂,奇怪,觉得特殊,而不是真的特殊。
+
运算符也是左折叠和右折叠效果一样,但是这个大家都懂,我就没提。另外上面提到的所谓的“逗号运算符本身什么都不做“是没有考虑你重载它的,这个大家知道就行。
二元折叠
template<typename... Args>
void print(Args&&... args){
(std::cout << ... << args) << '\n';
}
print("luse", 1, 1.2); // luse11.2
又是我们的老朋友 print
函数,不过这次,它又换了形式,这是一个二元左折叠。
判断一个折叠表达式是否是二元的,只需要看一点:运算符 ... 运算符
这种形式就是二元。
判断是左还是右,我们前面已经提了:
- 折叠表达式是左折叠还是右折叠,取决于
...
是在“形参包”的左边还是右边。
根据语法套一下 ( 初值 运算符 ... 运算符 形参包 )
,我们给出的示例 print 中 std::cout
就是初值、 运算符 ... 运算符
就是 << ... <<
、形参包就是 args
。
// 二元右折叠
template<int...I>
constexpr int v = (I + ... + 10); // 1 + (2 + (3 + (4 + 10)))
// 二元左折叠
template<int...I>
constexpr int v2 = (10 + ... + I); // (((10 + 1) + 2) + 3) + 4
std::cout << v<1, 2, 3, 4> << '\n'; // 20
std::cout << v2<1, 2, 3, 4> << '\n'; // 20
其实和一元折叠中说的差不多不是吗?
右折叠就是先算右边,左折叠就是先算左边。
不过二元折叠表达式必然有一个“初值”(我们这里是 10
),是先计算的。
总结
省略了一些,但这一节整体我写的较为满意,规则和重点都聊的很清楚,其他的任何形式,无非都是以上的去雕花罢了。
我们可以布置一个课后作业,说出以下代码使用的折叠表达式语法,以及它的效果,详细解析,使用 Markdown 语法。
template<class ...Args>
auto Reverse(Args&&... args) {
std::vector<std::common_type_t<Args...>> res{};
bool tmp{ false };
(tmp = ... = (res.push_back(args), false));
return res;
}
提交到 homework
文件夹中的 08折叠表达式作业
文件夹中(如果没有就创建),然后新建文件(命名需要是有意义的),写好后提交 pr。