首页 > 编程语言 >C++17 折叠表达式

C++17 折叠表达式

时间:2024-11-02 10:46:35浏览次数:5  
标签:std ... 17 折叠 args C++ && 表达式 op

折叠表达式

C++17之前处理参数包

C++17折叠表达式的出现让我们不必再用递归实例化模板的方式来处理参数包。

在 C++17之前,实现必须分成两个部分:

  • 终止条件
  • 递归条件

这样的实现不仅写起来麻烦,对 C++编译器来说也很难处理。C++17折叠表达式能显著的减少程序员和编译器的工作量

// 终止条件
// - 终止条件要写在递归条件之前
template<typename First>  
First foldSum1(First&& value)  
{  
    return value;  
}
// 递归条件
template<typename First, typename... Rest>  
First foldSum1(First&& first, Rest&&... rest)  
{  
    return first + foldSum1(std::forward<Rest>(rest)...);  
}
 
auto i1 = foldSum1(58, 25, 128, -10);  //201
auto s1 = foldSum1(std::string("a "), std::string("b "), std::string("c"));//"a b c"

C++17 折叠表达式 ... op

用途

自从 C++17起,新特性 折叠表达式 可以计算对参数包中的所有参数应用一个二元运算符的结果。

**目的:**不必再用递归实例化模板的方式来处理参数包(简化对C++11中引入的参数包的处理)

可以使代码更简洁、更易于阅读,但也可能会使代码更难以理解。需要谨慎使用,并考虑是否有其他更直观的方法可以达到相同的效果。

template<typename... T>
auto foldSum2(T... args)
{
    // 一元 无初值,不允许传递空参数包
    //  - 左折叠
    return (... + args); // ((arg1 + arg2) + arg3) ...
    //  - 右折叠
    return (args + ...); // ((arg3 + arg2) + arg1) ...
    // 二元 有初值,允许传递空参数包
    //  - 左折叠
	return (0 + ... + args); // 0 + ((arg1 + arg2) + arg3) ...
    //  - 右折叠
    return (args + ... + 0); // ((arg3 + arg2) + arg1) ... + 0
}
auto i2 = foldSum2(58, 25, 128, -10);  //201
auto s2 = foldSum2();//"a b c"

展开规则

折叠表达式共有四种语法形式:一元 二元,左折叠 右折叠 的两两组合。

  • 一元 二元:

    • 不存在/存在 初值

    • 对空参数包处理不同(二元一定允许,一元未必允许。见下文)

  • 左折叠 右折叠:

    • 初始值在 左边/右边
    • 表达式求值顺序,从 左边/右边 开始——展开之后从左边/右边开始折叠

示例中符号的含义如下:

  • op:运算符
  • args:参数包,含有未展开的形参包
  • value:初始值,不含未展开的形参包

注意事项

  • 优先选,左折叠表达式 (... + args);(value op ... op args);,因为对大多数人来说从左向右求值更符合直觉。
  • ()开闭括号也是折叠表达式的一部分,不能被省略
  • argsvalue不含优先级低于 转型表达式 的运算符的表达式。
# 1.一元左折叠(unary left fold)
# - 展开前
( ... op args )
# - 展开后
((arg_1 op arg_2) op ...) op arg_N
((1 + 2) + ...) + 9

# 2.二元左折叠(binary left fold)
# - 展开前
(value op ... op args)
# - 展开后
(((arg_value op arg_1) op arg_2) op ...) op arg_N
(((0 + 1) + 2) + ...) + 9


# 3.一元右折叠(unary right fold)
# - 展开前
(args op ...)
# - 展开后
arg_1 op (... op (arg_N-1 op arg_N))
1 + ( ... + (8 + 9))

# 4.二元右折叠(binary right fold)
# - 展开前
(args op ... op value)
# - 展开后
arg_1 op (... op (arg_N−1 op (arg_N op arg_value)))
1 + ( ... + (8 + (9 + 0))

sizeof…

template <class ...Ts>
constexpr auto get_common_type(Ts ...ts) {
    if constexpr (sizeof...(Ts) == 0) {}
    if constexpr (sizeof...(ts) == 0) {

处理空参数包

一元折叠

将一元折叠用于长度为零的包展开时:

运算符空包的值
逻辑与 &&true
逻辑或 `
逗号运算符,void()
所有其他的运算符格式错误,编译失败
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
二元折叠

一定允许空参数包。

合法的操作符 opt

二元运算符

二元运算符:合法的有32 个

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

注意:默认情况下 lambda以值返回对象,这意味着会创建参数的不必要的拷贝。

解决方法:显式指明返回类型为 const auto&或者 decltype(auto):

template<typename First, typename... Args>
void print (const First& firstarg, const Args&... args) {
    std::cout << firstarg;
    auto spaceBefore = [](const auto& arg) ‐> const auto& {
        std::cout << ' ';
        return arg;
    };
    (std::cout << ... << spaceBefore(args)) << '\n';
}

template<typename First, typename... Args>
void print (const First& firstarg, const Args&... args) {
    std::cout << firstarg;
    (std::cout << ... << [] (const auto& arg) ‐> decltype(auto) {
        std::cout << ' ';
        return arg;
    }(args)) << '\n';
}
二元操作符

所有的二元操作符

  • . ->[]
  • , 逗号运算符(左叠表达式可以使用)
折叠函数调用

折叠表达式可以用于逗号运算符 —— 因此可以在一条语句里进行多次函数调用。

// 对所有参数调用函数 foo()
template<typename... Types>
void callFoo(const Types&... args)
{
    (... , foo(args)); // 调用foo(arg1),foo(arg2),foo(arg3),...
    // 如果需要支持移动语义
    (... , foo(std::forward<Types>(args)));
    // 如果 foo()函数返回的类型重载了逗号运算符,那么代码行为可能会改变
    // 安全的做法: 把返回值转换为 void
    (... , (void)foo(std::forward<Types>(args))); // 调 用foo(arg1),foo(arg2),...
}

例如:组合哈希函数

template<typename T>
void hashCombine (std::size_t& seed, const T& val)
{
    seed ^= std::hash<T>()(val) + 0x9e3779b9 + (seed<<6) + (seed>>2);
}

template<typename... Types>
std::size_t combinedHashValue (const Types&... args)
{
    std::size_t seed = 0; // 初始化seed
    (... , hashCombine(seed, args)); // 链式调用hashCombine()
    return seed;
}

combinedHashValue ("Hi", "World", 42);
// hashCombine(seed, "Hi"), hashCombine(seed, "World"), hashCombine(seed, 42);

玩的骚一点,可以调用可变数量基类的成员函数

#include <iostream>
// 可变数量基类的模板 
template<typename... Bases>
class MultiBase : private Bases...
{
public:
    void print() {
        // 调用所有基类Bases...的print()函 数
        (... , Bases::print());
    }
};
struct A { void print() { std::cout << "A::print()\n"; }}
struct B { void print() { std::cout << "B::print()\n"; }}
struct C { void print() { std::cout << "C::print()\n"; }}
int main()
{
    MultiBase<A, B, C> mb;
    mb.print();
    // (A::print(), B::print()), C::print();
}
处理类型

配合 SFINAE 或者 concept,可以使用折叠表达式处理模板参数包。

像通常一样,运算符 &&会短路求值(出现第一个 false时就会停止运算)

例如:检查模板参数包Ts ...ts 与某一类型是否存在关系。

template <class T0, class ...Ts>
// 可以显式转换(过于宽松)
// requires ((true && ... && std::is_constructible_v<Ts, T0>))
// 可以隐式转换
requires ((true && ... && std::is_convertible_v<Ts, T0>))
// 完全相同(过于严格)
// requires ((true && ... && std::is_same_v<Ts, T0>))
std::array<T0, sizeof...(Ts) + 1> myvec(T0 t0, Ts ...ts) {
    return {t0, ts...};
}

例如:类型是否有任意一个可以转换

// - Ts中 是否有任意一个 可以与T转换
template <class T, class ...Ts>
struct is_convertible_any {
    static constexpr bool value = (false || ... || std::is_convertible_v<T, Ts>);
};

例如:类型是否有任意一个相同

// - Ts中 是否有任意一个 与T类型相同
template <class T, class ...Ts>
struct is_same_any {
    static constexpr bool value = (false || ... || std::is_same_v<T, Ts>);
};

标签:std,...,17,折叠,args,C++,&&,表达式,op
From: https://blog.csdn.net/weixin_41733034/article/details/143352160

相关文章

  • 多校 A 层冲刺 NOIP2024 模拟赛 17
    多校A层冲刺NOIP2024模拟赛17T1网格签到题注意到\(+\)与\(\times\)由于优先级其实是分开的,所以可以考虑每到达一个\(+\)计算一次贡献(乘上一个组合数),然后将前置贡献重新赋值为方案数,DP只需考虑连续\(\times\)段即可。时间复杂度\(O(nm)\)。T2矩形根号分治发现不......
  • 《 C++ 修炼全景指南:十八 》缓存系统的技术奥秘:LRU 原理、代码实现与未来趋势
    摘要本篇博客深入解析了LRU(LeastRecentlyUsed)缓存机制,包括其核心原理、代码实现、优化策略和实际应用等方面。通过结合双向链表与哈希表,LRU缓存实现了高效的数据插入、查找与删除操作。文章还对LRU的优化方案进行了详细讨论,包括在不同应用场景下的性能提升、内存优化......
  • C++ ──── 红黑树的实现
    目录1.红黑树的概念2.红黑树的性质3. 红黑树节点的定义4.红黑树的插入操作 5. 红黑树的验证6.红黑树的删除7. 红黑树与AVL树的比较8. 红黑树的应用总代码:1.红黑树的概念        红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结......
  • C/C++ 知识点:重载、覆盖和隐藏
    文章目录一、重载、覆盖和隐藏1、重载(overload)1.1、定义1.2、使用`const`关键字1.3、实现原理2、覆盖(override)2.1、定义2.2、覆盖的条件2.3、`override`关键字3、隐藏(hiding)3.1、定义3.2、隐藏的条件3.3、隐藏与覆盖的区别3.4、示例前言:在C++中多态性是一个......
  • c++:vector
    一、vector是什么?1.1vector的介绍vector是表示可变大小数组的序列容器。 就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。本质......
  • C++详细笔记(五)
    1.类和对象1.1运算符重载(补)1.运算符重载中,参数顺序和操作数顺序是一致的。2.一般成员函数重载为成员函数,输入流和输出流重载为全局函数。3.由1和2只正常的成员函数默认第一个参数为this指针而重载中参数顺序和操作数顺序要一致,则导致使用时为d<<cout;(不符合使用习惯正常为......
  • E74 树形DP P4657 [CEOI2017] Chase
    视频链接:E74树形DPP4657[CEOI2017]Chase_哔哩哔哩_bilibili  P4657[CEOI2017]Chase-洛谷|计算机科学教育新生态(luogu.com.cn)//树形DPO(n*m)#include<bits/stdc++.h>#defineLLlonglongusingnamespacestd;constintN=100010,M=110;intidx,he......
  • awk&&文本处理工具和正则表达式
    awk2.2.4优先级简单实践实践1-分结构实践BEGIN设定数据处理的前置准备[root@rocky9~]#awk'BEGIN{OFS=":"}{printNR,$0}'awk.txt1:nihaoawk1awk2awk32:nihaoawk4awk5awk63:nihaoawk7awk8awk9{}定制输出的内容样式[root@rocky9~]#awk'{print"第一列:&qu......