C++17 实用组件:string_view、span 与 filesystem
C++17 实用组件:string_view、span 与 filesystem
C++ 的零拷贝(zero-copy)设计哲学有一条清晰的演进路线:从 C 语言的裸指针,到 C++98 的引用,再到 C++17 的 string_view 和 C++20 的 span<T>。这条路的终点是:函数参数只描述"我读什么",而不参与所有权决策,也不触发拷贝和分配。
与零拷贝参数字段平行发展的,是标准库对操作系统设施的封装——C++17 的 <filesystem> 第一次让目录遍历、路径操作、文件属性查询成为了标准 C++,不再依赖 POSIX 或 Win32 API。
1. std::string_view:只读的字符串视图
1.1 动机
考虑一个接收字符串参数的函数:
// 坏:接收 const std::string& —— 当调用方持有 const char* 时会触发隐式构造 + 分配
size_t count_spaces(const std::string& s);
// 好:接收 const char* + 长度 —— 零分配,但丢失了 std::string 的便利
size_t count_spaces(const char* s, size_t len);
// 最好:std::string_view —— 零分配 + string 的全部便利
size_t count_spaces(std::string_view s);string_view 不过是 { const char* data; size_t size; } 加上 std::string 的全部只读成员函数(substr、find、starts_with(C++20) 等)。
1.2 零拷贝 substr
string_view::substr() 不拷贝数据——它只调整指针和长度:
std::string_view sv = "hello world";
auto sub = sv.substr(0, 5); // sub = "hello",没有分配,只是 {data+0, size=5}
auto rest = sv.substr(6); // rest = "world",{data+6, size=5}这对于解析类任务(JSON、HTTP、配置文件)是巨大的性能改善——你可以用 substr 递归地"切"字符串来构造解析树,整个过程零内存分配。
1.3 生命周期陷阱
string_view 不拥有数据。所有指针/引用的规则对 string_view 同样适用:
std::string_view dangerous() {
std::string local = "temporary";
return std::string_view(local); // 悬空!local 在函数返回时析构
}
std::string_view safe() {
static const std::string global = "permanent";
return std::string_view(global); // OK,global 的生存期是程序全程
}
// 在调用链中临时使用是安全的
void f(std::string_view sv) {
process(sv.substr(5, 3)); // OK,原始数据在 f 的调用者那里
}关键规则
string_view 适合作为函数参数类型和局部临时变量。永远不要把它作为长期存储的数据成员,除非你确保底层字符串的存活。
1.4 string_view 与 null 终止的陷阱
string_view 不保证 null 终止。不能把它直接传给 C API:
std::string_view sv = "hello world";
// puts(sv.data()); // 危险!sv.data() 不保证 null 终止
// 在"hello world" 这个例子中恰巧是 null 终止的(字面量),
// 但 sv.substr(0, 5).data() 就不保证了
// 正确的做法:
puts(std::string(sv).c_str()); // 通过 std::string 获取 null 终止保证2. std::span<T> (C++20):泛化的视图
span<T> 是 string_view 的泛化——它把"不拥有的连续数据视图"从 char 推广到了任意类型:
// 之前:需要重载或模板来支持多种连续容器类型
void process(const std::vector<int>& vec); // 只接受 vector
void process(const int* data, size_t size); // 需要分开传指针和长度
// 现在:span 统一接受数组、vector、array、initializer_list
void process(std::span<const int> data);span 的内部存储和 string_view 完全同构:
template<typename T>
class span {
T* data_;
size_t size_;
// ...
};2.1 动态 extent vs 静态 extent
std::span<int> s1; // 动态 extent:size 在运行时确定
std::span<int, 16> s2; // 静态 extent:size 是编译期常量 16
// 静态 extent 的优势:sizeof 更小(不需要存储 size 成员)
static_assert(sizeof(std::span<int, 16>) == sizeof(void*));2.2 子范围与边界安全
void safe_process(std::span<int> data) {
auto first_half = data.first(data.size() / 2); // 子范围
auto last_three = data.last(3);
auto middle = data.subspan(2, 4); // data[2..5]
}这些子范围操作全部是 O(1)、零拷贝——只调整 span 内部的指针和长度。
2.3 const 的正确姿势
void read_only(std::span<const int> data); // 不能修改元素
void mutable_access(std::span<int> data); // 可以修改元素
// span 本身的 const 几乎总是多余的
void foo(const std::span<int> data); // "span 不可变",但它本来就是值类型参数span 被设计为值语义——取 span 参数时直接按值传递,不要加 const&。
3. std::filesystem:标准文件操作
3.1 路径处理
namespace fs = std::filesystem;
fs::path p = "/home/user/docs/report.txt";
p.filename(); // "report.txt"
p.stem(); // "report"
p.extension(); // ".txt"
p.parent_path(); // "/home/user/docs"
p.root_directory(); // "/"
// 路径拼接
auto config = fs::path("/etc") / "app" / "config.json"; // "/etc/app/config.json"std::filesystem::path 在 POSIX 上内部使用 UTF-8 的 std::string,在 Windows 上使用 UTF-16 的 std::wstring。统一接口,跨平台路径语义。
3.2 目录遍历
for (auto const& entry : fs::directory_iterator("/path/to/dir")) {
std::cout << entry.path() << '\n';
if (entry.is_directory()) {
std::cout << " [dir]\n";
} else if (entry.is_regular_file()) {
std::cout << " [file, size=" << entry.file_size() << "]\n";
}
}
// 递归遍历
for (auto const& entry : fs::recursive_directory_iterator("/path/to/dir")) {
std::cout << entry.path() << '\n';
}3.3 文件操作
fs::copy("src.txt", "dst.txt"); // 拷贝文件
fs::copy("src.txt", "dst.txt", fs::copy_options::overwrite_existing);
fs::rename("old.txt", "new.txt"); // 重命名/移动
fs::remove("obsolete.txt"); // 删除文件
fs::remove_all("/tmp/scratch"); // 递归删除目录
fs::create_directory("new_dir"); // 创建目录
fs::create_directories("a/b/c/d"); // 递归创建
bool exists = fs::exists("some_path"); // 路径是否存在
uintmax_t size = fs::file_size("large_file.bin"); // 文件大小
auto ftime = fs::last_write_time("file.txt"); // 最后修改时间3.4 status 与 errc
std::error_code ec;
auto status = fs::status("maybe_missing.txt", ec);
if (ec) {
std::cerr << "Error: " << ec.message() << '\n';
return;
}C++17 的 filesystem 有两个错误处理模型:抛异常的版本(默认)和返回 error_code 的版本(带 std::error_code& 参数)。后者适合性能敏感或不能使用异常的代码路径。
4. 三类组件的设计哲学
string_view、span<T> 和 filesystem 代表了 C++ 标准库演进的两条主线:
| 主线 | 代表 | 核心原则 |
|---|---|---|
| 零拷贝参数 | string_view / span | 函数参数只声明"看什么",不表达所有权 |
| 平台抽象 | filesystem | 一次编写,POSIX/Win32 通跑 |
二者的共同目标是一致的:减少 C++ 代码对特定平台 API 和特定数据所有权模型的偶合。
总结
string_view:当你需要"只读地看一段字符串",用string_view做参数类型。它接管了const std::string&和const char*的两种历史写法。span<T>:它是string_view的泛化。接受数组、vector 或任何连续存储的子范围时,它是统一的零拷贝接口。filesystem:路径拼接、目录遍历、文件属性查询——不再需要#ifdef _WIN32和 POSIX 的分叉代码。