1:开篇
前面七篇我们覆盖了从语法糖、编译模型、模板编程到类型安全工具的全系列特性,核心是提升代码的简洁性、健壮性与开发效率。本篇作为系列收官,聚焦两个工程级大型组件:
- std::filesystem:标准化的跨平台文件系统库,统一了 Windows/Linux/macOS 的文件操作 API,告别手写平台相关代码和依赖 boost
- 并行算法:标准库算法的并行化扩展,给传统 STL 算法加上执行策略参数,几行代码就能利用多核 CPU 提升性能
两个组件都是 C++ 标准库向 "实用化、工程化" 演进的重要标志,让 C++ 在标准库层面就具备了完整的文件操作和并行计算能力。
2:std::filesystem跨平台文件系统库
1:历史痛点
在 C++17 之前,C++ 标准库完全没有文件系统操作能力,开发者只能依赖平台特定 API 或第三方库:
- Windows:使用 Win32 API(CreateFile、FindFirstFile 等),代码和 POSIX 不兼容
- Linux/macOS:使用 POSIX 接口(dirent.h、sys/stat.h 等),和 Windows 不兼容
- 第三方方案:引入 boost.filesystem 等库,增加依赖和编译成本
核心问题:
- 跨平台代码需要写大量条件编译,维护成本高
- 不同平台 API 风格差异大,学习成本高
- 路径处理、目录遍历等常用操作没有统一标准
C++17 将 boost.filesystem 纳入标准,形成了std::filesystem,提供了完全跨平台的文件系统操作能力。
2:核心类与命名空间
std::filesystem 定义在 <filesystem> 头文件中,通常命名空间别名为 fs 以简化书写:
cpp
#include <filesystem>
namespace fs = std::filesystem;
核心层次类
| 类别 | 类 / 类型 | 核心职责 |
|---|---|---|
| 路径层 | fs::path |
路径的解析、拼接、分解、转换,纯字符串操作,不访问文件系统 |
| 状态层 | fs::file_status |
封装文件类型和权限信息 |
| 条目层 | fs::directory_entry |
目录中的一个条目,包含路径 + 缓存的状态信息 |
| 遍历层 | fs::directory_iterator |
单层目录遍历迭代器 |
fs::recursive_directory_iterator |
递归目录遍历迭代器 | |
| 辅助类型 | fs::perms |
权限位掩码枚举 |
fs::space_info |
磁盘空间信息结构体 | |
fs::file_time_type |
文件时间戳类型 | |
fs::filesystem_error |
文件操作异常类 |
3:路径操作fs::path解析
fs::path 是文件系统库的核心,它封装了路径的所有操作,自动处理不同平台的路径分隔符(Windows 的\和 POSIX 的/)。
1:构造与赋值
cpp
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
// 1. 从字符串构造
fs::path p1 = "/home/user/documents";
fs::path p2("C:\\Users\\name\\file.txt");
// 2. 路径拼接:使用 / 运算符(推荐)
fs::path p3 = p1 / "subdir" / "file.txt";
// 自动处理分隔符,POSIX下得到 /home/user/documents/subdir/file.txt
// 3. 追加:使用 += 或 /=
fs::path p4 = "dir";
p4 += "_name"; // 字符串追加,得到 "dir_name"
p4 /= "subdir"; // 路径追加,得到 "dir_name/subdir"
return 0;
}
2:路径分解
fs::path 提供了丰富的分解方法,提取路径的各个组成部分:
cpp
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
fs::path p = "/home/user/docs/report.txt";
std::cout << "完整路径: " << p << '\n';
std::cout << "根路径: " << p.root_path() << '\n'; // "/"
std::cout << "根目录: " << p.root_directory() << '\n'; // "/"
std::cout << "根名称: " << p.root_name() << '\n'; // "" (Windows下是盘符)
std::cout << "相对路径: " << p.relative_path() << '\n'; // "home/user/docs/report.txt"
std::cout << "父路径: " << p.parent_path() << '\n'; // "/home/user/docs"
std::cout << "文件名: " << p.filename() << '\n'; // "report.txt"
std::cout << "主干名: " << p.stem() << '\n'; // "report"
std::cout << "扩展名: " << p.extension() << '\n'; // ".txt"
// 路径修改
p.replace_extension(".md"); // 替换扩展名,变为 report.md
p.replace_filename("new.txt"); // 替换文件名
// 规范化路径(消除 . 和 .. 以及多余分隔符)
fs::path messy = "/home/./user/../user/docs/./file.txt";
fs::path clean = fs::weakly_canonical(messy);
// 结果: /home/user/docs/file.txt
return 0;
}
3:路径迭代
fs::path还支持迭代遍历每一级路径分量
cpp
fs::path p = "/home/user/docs/file.txt";
for (const auto& component : p) {
std::cout << component << '\n';
}
// 输出:
// "/"
// "home"
// "user"
// "docs"
// "file.txt"
4:底层原理
fs::path 内部存储原生格式的路径字符串,并在操作时根据当前平台的规则进行解析:
- Windows 平台:识别盘符、支持 UNC 路径、分隔符为
\(也接受/) - POSIX 平台:根目录为
/、分隔符为/ - 所有路径操作都是纯字符串处理,不访问文件系统,性能极高
/运算符重载实现了智能拼接,自动处理分隔符的添加
4:文件状态与类型查询
文件状态查询是文件系统操作的基础,filesystem 库提供了完整的状态查询函数。
1:状态查询函数
| 函数 | 作用 | 是否跟随符号链接 |
|---|---|---|
fs::exists(p) |
文件 / 目录是否存在 | 是 |
fs::status(p) |
获取文件状态(类型 + 权限) | 是 |
fs::symlink_status(p) |
获取符号链接本身的状态 | 否 |
fs::is_regular_file(p) |
是否是普通文件 | 是 |
fs::is_directory(p) |
是否是目录 | 是 |
fs::is_symlink(p) |
是否是符号链接 | 否 |
fs::is_block_file(p) |
是否是块设备文件 | 是 |
fs::is_character_file(p) |
是否是字符设备文件 | 是 |
fs::is_fifo(p) |
是否是管道文件 | 是 |
fs::is_socket(p) |
是否是套接字文件 | 是 |
fs::is_other(p) |
是否是其他类型(设备、管道等) | 是 |
fs::is_empty(p) |
文件 / 目录是否为空 |
关键区别 :大部分查询函数默认跟随符号链接 ,只有 is_symlink() 和 symlink_status() 是针对符号链接本身的。这是非常重要的细节。
2:代码示例
cpp
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
void printFileType(const fs::path& p) {
std::cout << "文件: " << p.filename() << '\n';
std::cout << " 存在: " << fs::exists(p) << '\n';
if (fs::exists(p)) {
std::cout << " 普通文件: " << fs::is_regular_file(p) << '\n';
std::cout << " 目录: " << fs::is_directory(p) << '\n';
std::cout << " 符号链接: " << fs::is_symlink(p) << '\n';
std::cout << " 空: " << fs::is_empty(p) << '\n';
if (fs::is_regular_file(p)) {
std::cout << " 文件大小: " << fs::file_size(p) << " bytes\n";
}
}
}
int main() {
printFileType("test.txt");
printFileType("mydir");
printFileType("link_to_test"); // 符号链接
return 0;
}
3:file::file_status详解
fs::status() 和 fs::symlink_status() 返回 fs::file_status 对象,它封装了文件类型和权限信息:
cpp
fs::file_status status = fs::status("test.txt");
// 文件类型
fs::file_type type = status.type();
// type 可以是:
// fs::file_type::none - 状态无效
// fs::file_type::not_found - 文件不存在
// fs::file_type::regular - 普通文件
// fs::file_type::directory - 目录
// fs::file_type::symlink - 符号链接
// fs::file_type::block - 块设备
// fs::file_type::character - 字符设备
// fs::file_type::fifo - 管道
// fs::file_type::socket - 套接字
// fs::file_type::unknown - 存在但类型未知
// 权限
fs::perms perm = status.permissions();
5:权限管理详解
权限管理是文件系统库非常重要的功能,std::filesystem 提供了完整的权限查询和修改能力,设计完全兼容 POSIX 权限模型。
1:权限类型:fs::perms位标志枚举
fs::perms 是一个位掩码枚举类型 ,每一位代表一种权限,可以通过按位运算组合。数值和 Linux chmod 命令的八进制表示完全一致。
| 枚举值 | 八进制值 | 含义 |
|---|---|---|
none |
0 |
无任何权限 |
| 所有者(User)权限 | ||
owner_read |
0400 |
文件所有者读权限 |
owner_write |
0200 |
文件所有者写权限 |
owner_exec |
0100 |
文件所有者执行权限 |
owner_all |
0700 |
所有者全部权限(读 + 写 + 执行) |
| 组(Group)权限 | ||
group_read |
0040 |
所属组读权限 |
group_write |
0020 |
所属组写权限 |
group_exec |
0010 |
所属组执行权限 |
group_all |
0070 |
组全部权限 |
| 其他用户(Others)权限 | ||
others_read |
0004 |
其他用户读权限 |
others_write |
0002 |
其他用户写权限 |
others_exec |
0001 |
其他用户执行权限 |
others_all |
0007 |
其他用户全部权限 |
| 全部权限 | ||
all |
0777 |
所有用户全部权限 |
| 特殊权限位 | ||
set_uid |
04000 |
设置 UID 位(执行时以所有者身份运行) |
set_gid |
02000 |
设置 GID 位(执行时以组身份运行) |
sticky_bit |
01000 |
粘滞位(目录下只有所有者能删除文件) |
| 掩码位 | ||
mask |
07777 |
所有有效权限位的掩码 |
记忆技巧 :完全对应 Linux chmod 命令 ------0755 = 所有者读写执行 (7) + 组读执行 (5) + 其他读执行 (5),三位八进制数分别对应用户、组、其他。
2:位运算操作
因为是位掩码枚举,支持所有按位运算:
cpp
fs::perms p = fs::perms::owner_read | fs::perms::owner_write; // 组合权限
p |= fs::perms::owner_exec; // 添加执行权限
p &= ~fs::perms::owner_write; // 移除写权限
// 检查是否有某权限
bool can_read = (p & fs::perms::owner_read) != fs::perms::none;
bool can_write = (p & fs::perms::owner_write) != fs::perms::none;
3:权限查询
cpp
#include <filesystem>
#include <iostream>
#include <iomanip>
namespace fs = std::filesystem;
void printPermissions(const fs::path& p) {
fs::perms perm = fs::status(p).permissions();
// 八进制输出
std::cout << "文件: " << p.filename() << '\n';
std::cout << " 八进制权限: 0" << std::oct << std::setw(3) << std::setfill('0')
<< static_cast<unsigned>(perm & fs::perms::mask) << std::dec << '\n';
// 类似 ls -l 的 rwx 格式输出
auto rwx = [&](fs::perms r, fs::perms w, fs::perms x) {
std::cout << ((perm & r) != fs::perms::none ? 'r' : '-');
std::cout << ((perm & w) != fs::perms::none ? 'w' : '-');
std::cout << ((perm & x) != fs::perms::none ? 'x' : '-');
};
std::cout << " rwx格式: ";
rwx(fs::perms::owner_read, fs::perms::owner_write, fs::perms::owner_exec);
rwx(fs::perms::group_read, fs::perms::group_write, fs::perms::group_exec);
rwx(fs::perms::others_read, fs::perms::others_write, fs::perms::others_exec);
std::cout << '\n';
// 特殊权限位
std::cout << " setuid: " << ((perm & fs::perms::set_uid) != fs::perms::none) << '\n';
std::cout << " setgid: " << ((perm & fs::perms::set_gid) != fs::perms::none) << '\n';
std::cout << " sticky: " << ((perm & fs::perms::sticky_bit) != fs::perms::none) << '\n';
}
int main() {
printPermissions("test.txt");
printPermissions("/usr/bin/passwd"); // 典型的setuid文件
printPermissions("/tmp"); // 典型的sticky目录
return 0;
}
4:权限修改fs::permissions函数
cpp
// 函数签名
void permissions(const path& p, perms prms);
void permissions(const path& p, perms prms, std::error_code& ec) noexcept;
void permissions(const path& p, perms prms, perm_options opts);
void permissions(const path& p, perms prms, perm_options opts, std::error_code& ec) noexcept;
第三个参数perm_options控制修改方式
| 选项 | 含义 |
|---|---|
replace(默认) |
用 prms 完全替换原有权限 |
add |
在原有权限基础上添加 prms 中的权限位(按位或) |
remove |
在原有权限基础上移除 prms 中的权限位(按位与非) |
nofollow |
不跟随符号链接,修改符号链接本身的权限 |
cpp
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
fs::path p = "test.txt";
// 1. 完全替换:设置为 0644(所有者读写,其他只读)
fs::permissions(p,
fs::perms::owner_read | fs::perms::owner_write
| fs::perms::group_read | fs::perms::others_read);
// 2. 添加权限:给所有用户加上执行权限(等价于 chmod +x)
fs::permissions(p,
fs::perms::owner_exec | fs::perms::group_exec | fs::perms::others_exec,
fs::perm_options::add);
// 3. 移除权限:移除其他用户的写权限(等价于 chmod o-w)
fs::permissions(p, fs::perms::others_write, fs::perm_options::remove);
// 4. 不跟随符号链接,修改链接本身
fs::permissions("link.txt", fs::perms::owner_read,
fs::perm_options::replace | fs::perm_options::nofollow);
// 5. 错误码方式(推荐用于批量操作)
std::error_code ec;
fs::permissions("/etc/passwd", fs::perms::all, ec);
if (ec) {
std::cerr << "修改失败: " << ec.message() << '\n';
}
return 0;
}
5:跨平台差异与底层原理
POSIX 系统(Linux/macOS):
- 完全支持所有权限位,底层调用
stat()/lstat()查询,chmod()/lchmod()修改 - 特殊权限位(setuid、setgid、sticky)完全支持
- 符号链接本身的权限在大多数 POSIX 系统中无实际意义(始终是 0777),权限由目标文件决定
Windows 系统:
- Windows 的权限模型基于 ACL(访问控制列表),和 POSIX 差异很大,filesystem 库做了映射兼容
- 读 / 写权限:映射到 Windows 的 ACL 读写权限
- 执行权限:Windows 根据文件扩展名判断是否可执行,
.exe/.bat/.com等始终有执行权限,其他文件始终没有 - 组 / 其他用户权限:Windows 没有 POSIX 的三级权限模型,组和其他用户的权限通常和所有者一致
- 特殊权限位(setuid、setgid、sticky):Windows 不支持,设置无效
- 符号链接权限:Windows 不支持修改符号链接本身的权限
最佳实践 :跨平台代码中,只依赖 owner_read 和 owner_write 是最安全的;执行权限、组权限等在 Windows 上行为和 POSIX 不同。
6:文件与目录操作
filesystem 库提供了完整的文件和目录操作函数,全部跨平台可用。
1:创建与删除
cpp
#include <filesystem>
namespace fs = std::filesystem;
int main() {
// ===== 创建目录 =====
fs::create_directory("mydir"); // 创建单个目录,父目录必须存在
fs::create_directories("a/b/c/d"); // 递归创建所有层级目录
// create_directories 会自动创建所有不存在的父目录
// ===== 删除 =====
fs::remove("file.txt"); // 删除单个文件或空目录
fs::remove_all("mydir"); // 递归删除目录及其所有内容
// remove_all 返回删除的条目数量
// ===== 拷贝 =====
fs::copy("src.txt", "dst.txt"); // 拷贝文件
// 拷贝目录(需要指定选项)
fs::copy_options opts =
fs::copy_options::recursive | // 递归拷贝子目录
fs::copy_options::overwrite_existing | // 覆盖已存在文件
fs::copy_options::copy_symlinks; // 拷贝符号链接本身,不跟随
fs::copy("src_dir", "dst_dir", opts);
// copy_options 其他常用选项:
// skip_existing - 跳过已存在的文件
// update_existing - 只在源文件更新时覆盖
// directories::skip - 跳过目录
// create_symlinks - 创建符号链接代替拷贝
// hard_links - 创建硬链接代替拷贝
// ===== 重命名/移动 =====
fs::rename("old_name.txt", "new_name.txt");
// rename 也可以用来移动文件到不同目录
// ===== 创建符号链接/硬链接 =====
fs::create_symlink("target.txt", "link.txt"); // 符号链接
fs::create_hard_link("target.txt", "link.txt"); // 硬链接
return 0;
}
2:空间与时间查询
cpp
#include <filesystem>
#include <chrono>
namespace fs = std::filesystem;
int main() {
fs::path p = "test.txt";
// 文件大小(仅普通文件有效)
uintmax_t size = fs::file_size(p);
// 最后修改时间
fs::file_time_type ftime = fs::last_write_time(p);
// file_time_type 是 std::chrono::time_point 类型
// 修改最后修改时间
fs::last_write_time(p, fs::file_time_type::clock::now());
// 磁盘空间查询
fs::space_info si = fs::space("/");
std::cout << "总容量: " << si.capacity << " bytes\n";
std::cout << "空闲空间: " << si.free << " bytes\n";
std::cout << "可用空间: " << si.available << " bytes\n";
// free 是物理空闲空间,available 是当前用户可用的空间(可能更小)
// 可用空间考虑了磁盘配额、保留空间等因素
return 0;
}
7:目录遍历
目录遍历是文件系统操作的高频场景,filesystem库提供了两种迭代器
1:单层目录遍历:directory_iterator
fs::directory_iterator 遍历目录下的直接子条目,不进入子目录:
cpp
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
fs::path dir_path = ".";
// 方式1:范围for遍历(最常用)
for (const auto& entry : fs::directory_iterator(dir_path)) {
// entry 是 fs::directory_entry 类型
std::cout << entry.path().filename();
// directory_entry 会缓存状态,减少系统调用
if (entry.is_directory()) {
std::cout << " [目录]";
} else if (entry.is_regular_file()) {
std::cout << " " << entry.file_size() << " bytes";
}
std::cout << '\n';
}
// 方式2:迭代器遍历
auto it = fs::directory_iterator(dir_path);
auto end = fs::directory_iterator(); // 默认构造就是end迭代器
for (; it != end; ++it) {
// 处理每个条目
}
// 遍历选项
// directory_options::none - 默认行为
// directory_options::skip_permission_denied - 跳过无权限访问的条目
// directory_options::follow_directory_symlink - 跟随目录符号链接
return 0;
}
directory_entry的优化:
fs::directory_entry 不只是路径,它还缓存了文件状态信息。当你调用 entry.is_directory()、entry.file_size() 等方法时,如果缓存有效就不会发起系统调用,大幅提升目录遍历的性能。
2:递归目录遍历:recursive_directory_iterator
fs::recursive_directory_iterator 深度优先遍历所有子目录:
cpp
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
fs::path root = ".";
// 递归遍历所有文件和子目录
for (const auto& entry : fs::recursive_directory_iterator(root)) {
// depth() 获取当前递归深度(从0开始)
std::cout << "[" << entry.depth() << "] "
<< entry.path().relative_path() << '\n';
}
// 高级用法:控制遍历深度
auto it = fs::recursive_directory_iterator(root);
for (; it != fs::recursive_directory_iterator(); ++it) {
if (it.depth() >= 2) {
it.disable_recursion_pending();
// 跳过当前条目的子目录,不进入递归
}
if (it->is_directory() && it->path().filename() == ".git") {
it.pop(); // 直接弹出当前目录,跳过整个.git目录
}
}
// 递归遍历选项
// directory_options::skip_permission_denied - 跳过无权限的条目
// directory_options::follow_directory_symlink - 跟随目录符号链接(注意循环风险)
return 0;
}
递归迭代器的特殊方法:
depth():当前递归深度,根目录为 0pop():向上弹出一层,提前结束当前目录的遍历disable_recursion_pending():禁止进入当前条目的子目录
3:底层原理
- 目录迭代器底层封装了平台的目录遍历 API:Windows 的
FindFirstFile/FindNextFile,POSIX 的opendir/readdir directory_entry缓存了文件状态,避免每次查询都发起系统调用- 递归迭代器内部维护一个栈结构,模拟深度优先遍历:遇到目录时压栈,遍历完弹栈
- 迭代器是输入迭代器类别,只能单向遍历,不支持随机访问
8:错误处理
filesystem 库提供两种错误处理方式,适配不同场景:
cpp
#include <filesystem>
#include <system_error>
#include <iostream>
namespace fs = std::filesystem;
int main() {
// ===== 方式1:异常方式(默认) =====
// 操作失败抛出 fs::filesystem_error 异常
try {
fs::copy("nonexistent.txt", "dst.txt");
} catch (const fs::filesystem_error& e) {
std::cout << "错误信息: " << e.what() << '\n';
std::cout << "路径1: " << e.path1() << '\n';
std::cout << "路径2: " << e.path2() << '\n';
std::cout << "错误码: " << e.code() << '\n';
std::cout << "错误类别: " << e.code().category().name() << '\n';
}
// ===== 方式2:错误码方式 =====
// 传入 std::error_code& 参数,失败不抛异常,错误码写入参数
std::error_code ec;
fs::copy("nonexistent.txt", "dst.txt", ec);
if (ec) {
std::cout << "错误码值: " << ec.value() << '\n';
std::cout << "错误信息: " << ec.message() << '\n';
std::cout << "错误类别: " << ec.category().name() << '\n';
}
// 无错误时 ec.value() == 0,ec 转换为 bool 为 false
return 0;
}
最佳实践:
- 性能敏感的批量操作、预期可能失败的操作:优先用错误码方式,避免异常开销
- 不预期失败的关键操作、代码量少的场景:用异常方式,代码更简洁
- 递归遍历大目录时,推荐用错误码方式配合
skip_permission_denied选项
3:并行算法
1:历史痛点
C++11 引入了线程库,让 C++ 拥有了标准的多线程能力,但要让 STL 算法并行执行,开发者需要手动做很多工作:
- 手动拆分数据块
- 创建和管理线程池
- 同步各个线程的执行
- 合并计算结果
- 处理异常和边界情况
对于排序、查找、数值计算这类天然可并行的操作,手写并行版本的成本非常高,而且容易出错。很多开发者因为门槛太高,宁愿浪费多核性能也不愿意写并行代码。
C++17 将并行化能力直接集成到了标准库算法中,只需添加一个执行策略参数,就能让传统的 STL 算法并行执行,大幅降低了并行编程的门槛。
2:执行策略
并行算法通过第一个参数指定执行策略,定义在 <execution> 头文件中,共有三种标准策略:
| 执行策略 | 对应对象 | 行为 | 线程模型 |
|---|---|---|---|
| 顺序执行 | std::execution::seq |
单线程顺序执行 | 单线程 |
| 并行执行 | std::execution::par |
多线程并行执行 | 多线程 |
| 并行向量化 | std::execution::par_unseq |
并行 + 向量化,允许指令级并行 | 多线程 + SIMD |
cpp
#include <vector>
#include <algorithm>
#include <execution>
int main() {
std::vector<int> v(1000000);
// 传统顺序排序(和C++14完全一样)
std::sort(v.begin(), v.end());
// C++17 并行排序
std::sort(std::execution::par, v.begin(), v.end());
// 并行+向量化排序(性能最高)
std::sort(std::execution::par_unseq, v.begin(), v.end());
return 0;
}
三种策略的底层差异
-
seq(顺序执行):
- 和传统算法完全一致,单线程顺序执行
- 元素处理顺序有保证
- 作为基线对比,也用于不适合并行的小数据量
-
par(并行执行):
- 内部使用线程池,将数据拆分为多个块,分配给不同线程并行处理
- 不保证元素的处理顺序
- 函数对象可以使用线程局部存储、互斥锁等同步机制
- 异常会被捕获并重新抛出
-
par_unseq(并行向量化):
- 在并行的基础上,进一步允许向量化(使用 SIMD 指令)
- 允许重排执行顺序以最大化流水线效率
- 要求最高:函数对象不能调用任何同步原语(不能加锁、不能分配内存等)
- 性能最高,但限制也最多
注意:执行策略只是给编译器的提示,编译器可以选择降级执行(比如 par_unseq 降级为 par,par 降级为 seq)。主流编译器(GCC、Clang、MSVC)都有完整的并行实现。
3:并行支持的常用算法
1:排序与查找类
cpp
#include <vector>
#include <algorithm>
#include <execution>
#include <numeric>
int main() {
std::vector<int> v(1000000);
// ===== 排序算法 =====
std::sort(std::execution::par, v.begin(), v.end());
std::stable_sort(std::execution::par, v.begin(), v.end());
std::partial_sort(std::execution::par, v.begin(), v.begin() + 100, v.end());
std::nth_element(std::execution::par, v.begin(), v.begin() + 500, v.end());
// ===== 查找算法 =====
auto it = std::find(std::execution::par, v.begin(), v.end(), 42);
auto it2 = std::find_if(std::execution::par, v.begin(), v.end(),
[](int x) { return x > 100; });
auto count = std::count(std::execution::par, v.begin(), v.end(), 42);
auto count2 = std::count_if(std::execution::par, v.begin(), v.end(),
[](int x) { return x % 2 == 0; });
bool all_pos = std::all_of(std::execution::par, v.begin(), v.end(),
[](int x) { return x >= 0; });
bool any_neg = std::any_of(std::execution::par, v.begin(), v.end(),
[](int x) { return x < 0; });
bool none_zero = std::none_of(std::execution::par, v.begin(), v.end(),
[](int x) { return x == 0; });
// ===== 二分查找(注意:要求已排序) =====
// 二分查找本身是O(log n),并行收益不大,通常不需要并行
bool found = std::binary_search(v.begin(), v.end(), 42);
return 0;
}
2:数值计算类
cpp
#include <vector>
#include <numeric>
#include <execution>
int main() {
std::vector<double> v(1000000, 1.0);
// ===== 归约算法 =====
// reduce:并行版的 accumulate,不保证累加顺序
double sum = std::reduce(std::execution::par, v.begin(), v.end());
// 注意:浮点数求和顺序不同结果可能有微小差异!
// 带初始值的 reduce
double sum2 = std::reduce(std::execution::par, v.begin(), v.end(), 0.0);
// transform_reduce:变换+归约组合
double dot_product = std::transform_reduce(
std::execution::par,
v.begin(), v.end(), // 第一个序列
v.begin(), // 第二个序列
0.0 // 初始值
);
// 自定义操作的 transform_reduce
auto sum_of_squares = std::transform_reduce(
std::execution::par,
v.begin(), v.end(),
0.0,
std::plus<>(), // 归约操作:加法
[](double x) { return x * x; } // 变换操作:平方
);
// ===== 前缀和(扫描算法) =====
std::vector<double> result(v.size());
// inclusive_scan:包含当前元素的前缀和
std::inclusive_scan(std::execution::par,
v.begin(), v.end(),
result.begin());
// exclusive_scan:不包含当前元素的前缀和
std::exclusive_scan(std::execution::par,
v.begin(), v.end(),
result.begin(),
0.0); // 初始值
return 0;
}
重要 :std::reduce 和 std::accumulate 的区别:
accumulate:保证从左到右顺序累加,不支持并行reduce:不保证顺序,支持并行,结果可能和 accumulate 有微小差异(浮点数)- 整数运算结果一致,浮点数运算可能有精度差异
3:变换与遍历类
cpp
#include <vector>
#include <algorithm>
#include <execution>
int main() {
std::vector<int> input(1000000);
std::vector<int> output(1000000);
// ===== 变换算法 =====
std::transform(std::execution::par,
input.begin(), input.end(),
output.begin(),
[](int x) { return x * x; });
// 二元变换
std::vector<int> a(1000000), b(1000000), c(1000000);
std::transform(std::execution::par,
a.begin(), a.end(), b.begin(), c.begin(),
[](int x, int y) { return x + y; });
// ===== 遍历算法 =====
std::for_each(std::execution::par, input.begin(), input.end(),
[](int& x) { x *= 2; });
// for_each_n:遍历前n个元素
std::for_each_n(std::execution::par, input.begin(), 1000,
[](int& x) { x += 1; });
// ===== 复制与填充 =====
std::copy(std::execution::par, input.begin(), input.end(), output.begin());
std::fill(std::execution::par, output.begin(), output.end(), 0);
std::generate(std::execution::par, output.begin(), output.end(), std::rand);
return 0;
}
4:删除与去重类
cpp
#include <vector>
#include <algorithm>
#include <execution>
int main() {
std::vector<int> v(1000000);
// remove/remove_if:移除满足条件的元素
auto new_end = std::remove_if(std::execution::par,
v.begin(), v.end(),
[](int x) { return x < 0; });
v.erase(new_end, v.end());
// unique:去重(需要先排序)
std::sort(std::execution::par, v.begin(), v.end());
auto last = std::unique(std::execution::par, v.begin(), v.end());
v.erase(last, v.end());
// partition:分区
std::partition(std::execution::par, v.begin(), v.end(),
[](int x) { return x % 2 == 0; });
return 0;
}
4:性能对比测试
cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <execution>
#include <chrono>
#include <random>
#include <numeric>
template<typename Func>
double benchmark(Func&& func, int iterations = 5) {
double total = 0;
for (int i = 0; i < iterations; ++i) {
auto start = std::chrono::high_resolution_clock::now();
func();
auto end = std::chrono::high_resolution_clock::now();
total += std::chrono::duration<double, std::milli>(end - start).count();
}
return total / iterations;
}
int main() {
const int N = 10'000'000;
std::vector<int> v(N);
std::mt19937 gen(42);
std::generate(v.begin(), v.end(), gen);
std::cout << "数据量: " << N << " 个int\n";
std::cout << "==============================\n";
// ===== 排序性能对比 =====
auto v1 = v;
double t_sort_seq = benchmark([&]() {
auto tmp = v1;
std::sort(tmp.begin(), tmp.end());
});
double t_sort_par = benchmark([&]() {
auto tmp = v1;
std::sort(std::execution::par, tmp.begin(), tmp.end());
});
std::cout << "排序 - 顺序: " << t_sort_seq << "ms\n";
std::cout << "排序 - 并行: " << t_sort_par << "ms\n";
std::cout << "加速比: " << t_sort_seq / t_sort_par << "x\n\n";
// ===== 求和性能对比 =====
double t_sum_seq = benchmark([&]() {
volatile long long sum = std::accumulate(v.begin(), v.end(), 0LL);
});
double t_sum_par = benchmark([&]() {
volatile long long sum = std::reduce(std::execution::par, v.begin(), v.end(), 0LL);
});
std::cout << "求和 - 顺序: " << t_sum_seq << "ms\n";
std::cout << "求和 - 并行: " << t_sum_par << "ms\n";
std::cout << "加速比: " << t_sum_seq / t_sum_par << "x\n\n";
// ===== 变换性能对比 =====
std::vector<int> output(N);
double t_trans_seq = benchmark([&]() {
std::transform(v.begin(), v.end(), output.begin(),
[](int x) { return x * x + 2 * x + 1; });
});
double t_trans_par = benchmark([&]() {
std::transform(std::execution::par, v.begin(), v.end(), output.begin(),
[](int x) { return x * x + 2 * x + 1; });
});
std::cout << "变换 - 顺序: " << t_trans_seq << "ms\n";
std::cout << "变换 - 并行: " << t_trans_par << "ms\n";
std::cout << "加速比: " << t_trans_seq / t_trans_par << "x\n";
return 0;
}
典型结果(8 核 CPU):
- 排序:并行约为顺序的 4~6 倍
- 求和:并行约为顺序的 5~7 倍
- 变换:并行约为顺序的 6~8 倍
实际加速比取决于:CPU 核心数、数据量、算法类型、内存带宽、编译器实现等因素。小数据量下并行可能反而更慢,因为线程创建和同步有开销。
5:底层原理
并行算法的底层实现因标准库而异,但核心思想一致:
- 任务拆分:将数据范围拆分为多个子块,每个子块作为一个独立任务
- 线程池调度:使用内部线程池执行任务,避免频繁创建销毁线程
- 负载均衡:动态调度任务,确保各个线程负载均衡(通常用工作窃取算法)
- 结果合并:所有任务完成后,合并各个子块的结果
主流实现:
- GCC (libstdc++):基于 Intel TBB (Threading Building Blocks) 实现,需要链接 tbb 库
- MSVC (MSVC STL):基于 PPL (Parallel Patterns Library) 实现,Windows 内置
- Clang (libc++):支持多种后端,常用 TBB
par_unseq 的向量化:
- 编译器自动生成 SIMD 指令(SSE/AVX/AVX-512 等)
- 一次处理多个数据,进一步提升计算密集型操作的性能
- 要求数据连续、操作无依赖,才能有效向量化
4:实战场景
1:遍历目录统计文件大小
filesystem 的递归遍历配合数值计算,几行代码实现目录大小统计:
cpp
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
uintmax_t calculateDirectorySize(const fs::path& dir) {
uintmax_t total_size = 0;
uintmax_t file_count = 0;
uintmax_t dir_count = 0;
std::error_code ec;
for (const auto& entry : fs::recursive_directory_iterator(
dir, fs::directory_options::skip_permission_denied, ec)) {
if (entry.is_regular_file()) {
total_size += entry.file_size();
file_count++;
} else if (entry.is_directory()) {
dir_count++;
}
}
std::cout << "文件数: " << file_count << '\n';
std::cout << "目录数: " << dir_count << '\n';
std::cout << "总大小: " << total_size << " bytes ("
<< total_size / 1024 / 1024 << " MB)\n";
return total_size;
}
2:大规模数据的并行处理
并行算法处理大数据集,比如批量日志分析:
cpp
#include <vector>
#include <algorithm>
#include <execution>
#include <string>
struct LogEntry {
std::string message;
int level; // 0:debug, 1:info, 2:warn, 3:error
uint64_t timestamp;
};
// 并行过滤错误日志
std::vector<LogEntry> filterErrors(const std::vector<LogEntry>& logs) {
std::vector<LogEntry> errors;
std::copy_if(std::execution::par,
logs.begin(), logs.end(),
std::back_inserter(errors),
[](const LogEntry& e) { return e.level >= 3; });
return errors;
}
// 并行统计各等级日志数量
std::vector<size_t> countByLevel(const std::vector<LogEntry>& logs) {
std::vector<size_t> counts(4, 0);
std::for_each(std::execution::par, logs.begin(), logs.end(),
[&](const LogEntry& e) {
// 注意:这里有数据竞争!实际使用需要原子操作或分块统计
// counts[e.level]++;
});
// 正确做法:用 transform_reduce 或者分块统计
return counts;
}
注意:并行 for_each 中不能直接修改共享变量,会有数据竞争。需要用原子操作、分块归约等方式正确同步。
5:场景陷阱与最佳实践
1:filesystem陷阱与最佳实践
1:常见陷阱
- 路径编码问题 :Windows 下路径是宽字符(wchar_t),POSIX 下是 char,跨平台处理中文路径要注意编码转换,优先用
u8path或统一 UTF-8 - 符号链接循环:递归遍历时如果开启了跟随目录符号链接,可能遇到符号链接指向父目录导致无限循环,默认不跟随是安全的
- 权限不足静默失败:文件操作可能因为权限不足失败,关键操作务必处理错误,不要假设一定成功
- 目录执行权限的含义 :目录的「执行权限」不是运行程序,而是能否进入目录、访问目录内的文件,这是 POSIX 的经典概念,很多新手容易搞混
- status vs symlink_status :大部分函数默认跟随符号链接,想操作链接本身要用
symlink_status和nofollow选项 - file_size 对目录无效 :
file_size()只能用于普通文件,目录的 "大小" 需要递归统计所有文件
2:最佳实践
- 路径操作优先用
/运算符拼接,不要手动拼字符串,避免分隔符错误 - 批量操作优先用错误码方式,避免异常开销,配合
skip_permission_denied选项 - 递归遍历大目录时,利用
directory_entry的缓存状态减少系统调用 - 跨平台代码只依赖
owner_read和owner_write权限,兼容性最好 - 操作符号链接时,明确你是想修改链接本身还是目标文件,选择对应的选项
2:并行算法陷阱与最佳实践
1:常见陷阱
- 不要盲目并行:小数据量下并行的开销(线程创建、同步)可能超过收益,通常百万级以上数据才有明显加速。先测量再决定是否并行
- 注意浮点精度:并行归约的求和顺序不固定,浮点数累加结果可能和顺序版本有微小差异。精度敏感的计算要注意
- lambda 必须线程安全:不要在 lambda 中修改共享变量,或者确保正确同步。数据竞争是并行算法最常见的 bug
- 异常会终止程序 :并行算法中的异常会导致
std::terminate终止整个程序,不要在并行 lambda 中抛出异常 - par_unseq 限制最严格 :
par_unseq策略下,函数对象不能调用任何同步原语(不能加锁、不能分配内存、不能调用任何可能阻塞的函数),否则是未定义行为 - 迭代器有效性:并行执行期间,迭代器指向的范围必须保持有效,不能在并行算法执行时修改容器大小
2:最佳实践
- 排序、归约这类计算密集型、大数据量的场景优先考虑并行
- I/O 密集型场景并行收益有限,不要滥用
- 优先使用
par策略,限制更少,更安全;par_unseq只在确认符合要求且需要极致性能时使用 - 优先使用标准库提供的并行算法(reduce、transform_reduce 等),不要自己手写并行 for 循环累加
- 小数据量直接用顺序版本,不要为了并行而并行
6:C++17全系列总结与学习建议
1:全系列回顾
本系列 8 篇博客,覆盖了 C++17 的核心特性,按照从基础到进阶的顺序循序渐进
| 篇数 | 主题 | 核心特性 | 定位 |
|---|---|---|---|
| 第 1 篇 | 语法糖篇 | 结构化绑定 + if/switch 初始化 | 提升代码可读性 |
| 第 2 篇 | 编译模型篇 | inline 变量 + 强制拷贝省略 | 解决 ODR 痛点,提升性能 |
| 第 3 篇 | 模板编程篇 | if constexpr + 折叠表达式 | 简化模板元编程 |
| 第 4 篇 | 模板简化篇 | CTAD + 非类型模板参数 auto + 嵌套命名空间 + __has_include | 工程化语法补充 |
| 第 5 篇 | 健壮性篇 | 标准属性 + 新求值顺序 | 减少隐性 bug |
| 第 6 篇 | 类型安全上篇 | optional + variant | 类型安全的可空值与联合体 |
| 第 7 篇 | 类型安全下篇 | any + string_view | 通用类型容器与零拷贝字符串 |
| 第 8 篇 | 大型组件篇 | filesystem + 并行算法 | 跨平台文件操作与多核并行 |
2:学习建议
- 先掌握高频特性:结构化绑定、if 初始化、string_view、optional 这些日常开发高频使用的特性,优先掌握并应用到代码中,立竿见影提升代码质量
- 理解底层原理:不要停留在语法层面,理解每个特性的底层实现机制,才能避免踩坑,写出真正高效的代码
- 循序渐进:模板元编程、并行算法这类进阶特性,可以在掌握基础后再深入学习,不要一开始就啃硬骨头
- 实践出真知:写代码验证每个特性,观察编译结果和运行行为,动手实践是最好的学习方式
- 逐步迁移:不要想着一次性把所有 C++17 特性都用上,可以从新项目开始,逐步引入新特性,老项目按需迁移
3:后续学习方向
掌握 C++17 之后,可以继续学习 C++20 的革命性特性,C++20 是继 C++11 之后最大的一次标准更新:
- 概念 (Concepts):模板的类型约束,彻底改善模板报错信息,让泛型编程更易用
- 范围 (Ranges):管道式的算法组合,比传统 STL 算法更强大、更易读
- 协程 (Coroutines):异步编程的语法级支持,大幅简化异步代码,告别回调地狱
- 模块 (Modules):替代头文件的全新编译单元,解决头文件地狱问题,大幅提升编译速度
- 三路比较运算符
<=>:一行代码生成所有比较运算符 - 格式化库 (std::format):类型安全的格式化字符串,替代 printf 和 iomanip
- 日历与时区库:完整的日期时间处理能力