C++17文件系统库和并行算法
C++17文件系统库和并行算法
星野暗涌📚 导读
了解为什么要关注 C++17 的
std::filesystem和并行算法快速掌握常用 API 与并行执行策略
通过实战示例感受“文件系统 + 并行”的威力
📌 一、为什么要关心 C++17 的这两部分?
在 C++17 之前,标准库对 文件系统操作 和 并行计算 支持都比较薄弱:
操作文件和目录通常依赖平台 API(如 POSIX、Win32)或 Boost.Filesystem。
想利用多核 CPU,往往要手写线程池或使用第三方库。
C++17 引入了两块关键能力:
std::filesystem:跨平台的标准文件系统库。std::execution并行算法:在熟悉的<algorithm>基础上,通过执行策略轻松启用并行。
这两者结合,可以很方便地写出“既能遍历磁盘、又能吃满多核 CPU”的实用程序。
📁 二、std::filesystem:现代 C++ 的文件系统库
🔧 2.1 基本概念与命名空间
概念讲解
头文件:
<filesystem>命名空间:
std::filesystem(通常起个别名fs)常用类型:
fs::path:表示路径(文件 / 目录)
fs::directory_entry:目录中的一个条目fs::directory_iterator:遍历目录fs::recursive_directory_iterator:递归遍历目录树
代码示例
1 |
|
🧱 2.2 fs::path:优雅处理路径
概念讲解
fs::path 用来安全、可移植地表示路径,支持拆分、拼接和转换字符串。
常见需求:
获取父目录 / 文件名 / 后缀名
构造多级路径
与
std::string互转
代码示例
1 | fs::path p{"./data/file.txt"}; |
1 | std::string s = p.string(); |
📂 2.3 文件与目录的基本操作
概念讲解
常见操作:
检查路径是否存在、是否为文件 / 目录
创建目录(含递归创建)
删除文件或整个目录树
拷贝、重命名、移动
代码示例
1 | fs::path p{"example.txt"}; |
1 | // 创建目录 |
1 | fs::path p{"example.txt"}; |
1 | // 创建目录 |
1 | // 拷贝文件(若存在则覆盖) |
1 | for (const auto &entry : fs::directory_iterator{"."}) { |
🔍 2.4 遍历目录与递归遍历
概念讲解
使用
fs::directory_iterator遍历当前目录使用
fs::recursive_directory_iterator递归遍历目录树搭配条件判断筛选特定后缀(如
.txt/.log)
代码示例
1 | for (const auto &entry : fs::recursive_directory_iterator{"./data"}) { |
1 |
|
🛡️ 2.5 文件状态与权限:fs::status + fs::permissions
概念讲解
fs::file_status:保存文件的“类型 + 权限”等元信息fs::status(path):访问实际文件系统,返回file_status常见文件类型判断:
fs::is_regular_file(st)
fs::is_directory(st)fs::is_symlink(st)
权限使用 fs::perms 枚举表示,例如:
owner_read / owner_write / owner_execgroup_read / group_write / group_execothers_read / others_write / others_exec
修改权限使用 fs::permissions(path, perms, fs::perm_options::xxx)
replace:直接覆盖现有权限add:在原有基础上增加remove:在原有基础上移除
代码示例:查看类型 + 打印权限 + 修改权限
1 |
|
小贴士:Windows 下权限本质是 ACL,
fs::perms只是一个抽象映射;想做精细权限控制时,仍需要调用平台专用 API。
📏 2.6 示例:统计目录下所有文件总大小(顺序版本)
1 |
|
这已经是很多工具脚本的雏形了,后面我们会用并行算法把这种任务“多核加速”。
⚡ 三、C++17 并行算法与 <execution>
🚦 3.1 执行策略(Execution Policy)
概念讲解
C++17 定义了三种主要执行策略:
std::execution::seq:顺序执行,行为与传统<algorithm>一致。std::execution::par:允许并行执行,通常使用多个线程。std::execution::par_unseq:并行 + 向量化(SIMD),要求算法对顺序不敏感且无数据竞争。
代码示例
1 | init op a[0] op a[1] op ... op a[n-1] |
1 |
|
📚 3.2 并行算法到底在干什么?(核心概念)
可以把并行算法 + 执行策略理解成:
把「一段区间上的同构操作」拆分成多块,交给多个线程同时处理,最后把各块结果合并。
输入仍然是一对迭代器
[first, last),比如vector/array/ 普通数组算法本身还是熟悉的那几个:
sort/for_each/transform/reduce/transform_reduce等区别只在于:
顺序策略:按迭代顺序,一个元素一个元素处理
- 并行策略:把区间切成若干子区间,在不同线程上分别处理,再用“归约操作”合并
reduce 是 C++17 新增的“可并行求和(或一般归约)”算法,语义上是:
1 | // 1. transform 到一个中间容器 |
但具体分块方式、合并顺序都不保证,所以要求 op 满足:
交换律:
a op b == b op a结合律:
(a op b) op c == a op (b op c)
典型用法:
1 |
|
小结:当你只是要把一堆数字、结构体中的某个数值字段做「求和 / 求积 / min / max」这类操作时,
reduce是首选。
很多时候你希望:
先把元素变成某个标量(例如:
长度、平方、大小)再对这些标量做
reduce
传统写法通常要两步:
1 | std::uintmax_t total_size = std::transform_reduce( |
std::transform_reduce 把这两步合成一步,并且对 transform + reduce 整体做并行优化:
1 |
|
在我们「统计目录树总大小」的示例中:
- 变换函数:
fs::path -> fs::file_size(path)
1 |
|
一句话:
**reduce**:对“原始元素”做折叠
**transform_reduce**:对“经过一个映射后的值”做折叠,少一次中间容器,适合「路径 -> 文件大小」「结构体 -> 某个字段」这类场景。
📚 3.3 哪些算法支持并行执行策略?
大部分“元素遍历 + 计算”类算法都提供了执行策略重载,例如:
std::for_eachstd::transformstd::sort/std::stable_sortstd::reduce(C++17 新增)std::transform_reducestd::count,std::count_ifstd::find,std::find_ifstd::inclusive_scan/std::exclusive_scan等
1 |
|
⚠️ 3.4 使用并行算法的注意事项
避免数据竞争:Lambda 中不要无保护地写同一共享变量。
不要依赖顺序:
par/par_unseq下执行顺序未指定,涉及严格顺序 IO 不适合使用。数据量足够大再并行:小数据集上,并行开销可能超过收益。
🚀 四、std::filesystem + 并行算法:实战示例
📊 4.1 并行统计目录树中文件大小总和
思路:
用
std::filesystem递归遍历目录,收集所有文件路径。用
std::transform_reduce+std::execution::par并行计算总大小。
1 |
|
在这个例子中:
std::filesystem负责与 OS 打交道。std::transform_reduce负责并行把“路径 → 大小”的映射和值累加起来。
📈 4.2 并行统计某种扩展名文件的数量与平均大小
例如:统计目录下所有 .log 文件数量和平均大小。
1 |
|
你可以轻松把这个逻辑扩展为更复杂的分析(例如区分不同子目录、过滤时间、结合正则匹配文件名等)。
📑 五、常用 std::filesystem / 并行算法函数速查表
| 函数 | 作用概述 |
|---|---|
| 路径与基本信息 | |
fs::current_path() |
获取或设置当前工作目录 |
fs::path 构造 / /= |
表示路径;使用 /= 跨平台拼接子路径 |
p.parent_path() / p.filename() |
获取父目录 / 文件名部分 |
p.stem() / p.extension() |
获取不带扩展名的文件名 / 扩展名 |
fs::absolute(p) / fs::canonical(p) |
转为绝对路径;后者要求路径存在并做规范化 |
fs::relative(target, base) |
计算从 base 到 target 的相对路径 |
| 文件与目录操作 | |
fs::exists(p) |
判断路径是否存在 |
fs::is_regular_file(p) / fs::is_directory(p) |
判断是否为普通文件 / 目录 |
fs::file_size(p) |
获取文件大小(字节) |
fs::last_write_time(p) |
获取 / 设置最后修改时间 |
fs::create_directory(p) / fs::create_directories(p) |
创建单层 / 多层目录 |
fs::copy_file(from, to, opts) / fs::copy(from, to, opts) |
拷贝单个文件 / 拷贝整个目录树(可递归 + 覆盖) |
fs::remove(p) / fs::remove_all(p) |
删除文件或空目录 / 递归删除目录树 |
| 状态与权限 | |
fs::file_status / fs::status(p) |
获取文件状态(类型 + 权限) |
fs::perms |
权限枚举(owner/group/others 的 r/w/x) |
fs::permissions(p, perms, opts) |
按选项 replace/add/remove 设置或修改权限 |
fs::is_symlink(st) / fs::is_empty(p) |
判断是否为符号链接 / 文件或目录是否为空 |
fs::directory_options::skip_permission_denied |
在递归遍历时跳过权限不足的路径 |
| 目录遍历 | |
fs::directory_iterator(dir) |
遍历目录的直接子项(非递归) |
fs::recursive_directory_iterator(dir) |
递归遍历目录树 |
begin(it) / end(it) |
与范围 for 等价的显式迭代器遍历方式 |
fs::directory_entry |
目录中的单个条目,提供 path()、is_directory() 等接口 |
| 并行算法与执行策略 | |
std::execution::seq / par / par_unseq |
顺序 / 并行 / 并行 + SIMD 的执行策略 |
std::sort(policy, first, last) |
按给定策略排序(支持 seq / par / par_unseq) |
std::reduce(policy, first, last, init) |
对区间做归约(求和、求积、min/max 等),可并行 |
std::transform_reduce(policy, first, last, init, binary_op, unary_op) |
先用 unary_op 映射元素,再用 binary_op 做归约,常用于“映射 + 累加” |
std::for_each(policy, first, last, f) |
对区间内每个元素执行操作 f,可并行(注意避免数据竞争) |
std::count / std::count_if / std::find_if 等 |
按策略执行的计数 / 查找类算法,适合大数据集并行扫描 |
到这里,std::filesystem 的常见用法、权限/状态操作,以及与 C++17 并行算法(特别是 reduce / transform_reduce)的组合使用,就基本成体系了,可以直接拿去写工具脚本、日志分析器或者磁盘扫描类的小项目。
优先考虑使用 C++17 并行算法的场景:
任务可以抽象为“对一个范围内的元素做同构操作”。
不需要严格的处理顺序。
任务偏 CPU 密集(计算量较大)。
更适合自定义线程池 / 任务系统的场景:
需要复杂的任务依赖关系。
需要精细控制线程数量、绑定等。
涉及大量 IO、多种异步事件(网络、定时器等)。
如果要把这篇内容整理成博客,可以按下面的结构:
背景动机:C++17 解决哪些痛点(文件系统操作、并行计算)。
**std::filesystem**基本用法:path、遍历、创建 / 删除、拷贝等。并行算法与执行策略:
std::execution三种策略 + 常用算法。综合示例:用
std::filesystem收集数据,用并行算法处理。实践注意事项:数据竞争、性能、平台支持等。






