C++(八):文件系统库+并行算法

1:开篇

前面七篇我们覆盖了从语法糖、编译模型、模板编程到类型安全工具的全系列特性,核心是提升代码的简洁性、健壮性与开发效率。本篇作为系列收官,聚焦两个工程级大型组件

  1. std::filesystem:标准化的跨平台文件系统库,统一了 Windows/Linux/macOS 的文件操作 API,告别手写平台相关代码和依赖 boost
  2. 并行算法:标准库算法的并行化扩展,给传统 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_readowner_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():当前递归深度,根目录为 0
  • pop():向上弹出一层,提前结束当前目录的遍历
  • 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;
}

三种策略的底层差异

  1. seq(顺序执行)

    • 和传统算法完全一致,单线程顺序执行
    • 元素处理顺序有保证
    • 作为基线对比,也用于不适合并行的小数据量
  2. par(并行执行)

    • 内部使用线程池,将数据拆分为多个块,分配给不同线程并行处理
    • 不保证元素的处理顺序
    • 函数对象可以使用线程局部存储、互斥锁等同步机制
    • 异常会被捕获并重新抛出
  3. 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::reducestd::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:底层原理

并行算法的底层实现因标准库而异,但核心思想一致:

  1. 任务拆分:将数据范围拆分为多个子块,每个子块作为一个独立任务
  2. 线程池调度:使用内部线程池执行任务,避免频繁创建销毁线程
  3. 负载均衡:动态调度任务,确保各个线程负载均衡(通常用工作窃取算法)
  4. 结果合并:所有任务完成后,合并各个子块的结果

主流实现

  • 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:常见陷阱
  1. 路径编码问题 :Windows 下路径是宽字符(wchar_t),POSIX 下是 char,跨平台处理中文路径要注意编码转换,优先用 u8path 或统一 UTF-8
  2. 符号链接循环:递归遍历时如果开启了跟随目录符号链接,可能遇到符号链接指向父目录导致无限循环,默认不跟随是安全的
  3. 权限不足静默失败:文件操作可能因为权限不足失败,关键操作务必处理错误,不要假设一定成功
  4. 目录执行权限的含义 :目录的「执行权限」不是运行程序,而是能否进入目录、访问目录内的文件,这是 POSIX 的经典概念,很多新手容易搞混
  5. status vs symlink_status :大部分函数默认跟随符号链接,想操作链接本身要用 symlink_statusnofollow 选项
  6. file_size 对目录无效file_size() 只能用于普通文件,目录的 "大小" 需要递归统计所有文件
2:最佳实践
  • 路径操作优先用 / 运算符拼接,不要手动拼字符串,避免分隔符错误
  • 批量操作优先用错误码方式,避免异常开销,配合 skip_permission_denied 选项
  • 递归遍历大目录时,利用 directory_entry 的缓存状态减少系统调用
  • 跨平台代码只依赖 owner_readowner_write 权限,兼容性最好
  • 操作符号链接时,明确你是想修改链接本身还是目标文件,选择对应的选项

2:并行算法陷阱与最佳实践

1:常见陷阱
  1. 不要盲目并行:小数据量下并行的开销(线程创建、同步)可能超过收益,通常百万级以上数据才有明显加速。先测量再决定是否并行
  2. 注意浮点精度:并行归约的求和顺序不固定,浮点数累加结果可能和顺序版本有微小差异。精度敏感的计算要注意
  3. lambda 必须线程安全:不要在 lambda 中修改共享变量,或者确保正确同步。数据竞争是并行算法最常见的 bug
  4. 异常会终止程序 :并行算法中的异常会导致 std::terminate 终止整个程序,不要在并行 lambda 中抛出异常
  5. par_unseq 限制最严格par_unseq 策略下,函数对象不能调用任何同步原语(不能加锁、不能分配内存、不能调用任何可能阻塞的函数),否则是未定义行为
  6. 迭代器有效性:并行执行期间,迭代器指向的范围必须保持有效,不能在并行算法执行时修改容器大小
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
  • 日历与时区库:完整的日期时间处理能力
相关推荐
m0_519196401 小时前
【设计模式】java的习题
开发语言·python
王老师青少年编程1 小时前
2026年全国青少年信息素养大赛算法应用主题赛C++赛项【决赛】模拟卷(汇总)
c++·模拟卷·2026年·青少年信息素养大赛·算法应用主题赛·决赛
我命由我123451 小时前
Jetpack Compose - Material Design 断点范围、WindowSizeClass、针对不同屏幕尺寸创建预览、四类导航栏
android·java·开发语言·java-ee·kotlin·android jetpack·android runtime
captain3761 小时前
多线程进阶
java·开发语言·数据库
郑州光合科技余经理8 小时前
代驾系统架构拆解:订单链路、权限组织与私有化源码交付
开发语言·后端·算法·架构·系统架构·uni-app·php
码哥DFS11 小时前
二叉树的直径
开发语言·javascript·算法
paopaokaka_luck11 小时前
基于springboot3+vue3的企业考勤管理系统(部门树递归、Echarts图形化分析)
开发语言·spring boot·学习·echarts·mybatis·需求分析·代码规范
梦雨生生13 小时前
java开发工具(学习第一天)
java·开发语言·学习
啊啊啊啊啊!!!!14 小时前
【c++】stack和queue的接口使用以及底层实现
开发语言·c++