在 C++ 17 标准中,std::filesystem::path 被设计为一个轻量级、可移动、可复制的字符串包装器,用于表示文件系统路径。由于路径字符串可能很长且频繁被传递,移动语义的实现显得尤为重要。本文将深入探讨 std::filesystem::path 的移动构造和移动赋值运算符,以及如何通过自定义实现安全的深拷贝。
1. 设计目标
- 轻量级:
path对象内部应保持最小开销,避免不必要的堆分配。 - 可移动:移动操作应为 O(1),仅需移动指针或引用计数,而非完整拷贝字符串。
- 安全拷贝:拷贝操作应在必要时深拷贝,保证独立的内部状态,避免共享指针导致的内存泄漏或悬挂指针。
2. 典型实现思路
下面给出一种可能的实现方案,演示如何通过共享字符串(引用计数)与移动语义结合,实现高效且安全的路径对象。
#include <string>
#include <memory>
#include <cstring>
class Path {
public:
// 构造
Path(const char* p) : data_(new std::string(p)) {}
Path(const std::string& s) : data_(new std::string(s)) {}
Path(Path&& other) noexcept : data_(std::move(other.data_)) { other.data_ = nullptr; }
Path(const Path& other) : data_(new std::string(*other.data_)) {}
// 赋值
Path& operator=(Path&& other) noexcept {
if (this != &other) {
data_ = std::move(other.data_);
other.data_ = nullptr;
}
return *this;
}
Path& operator=(const Path& other) {
if (this != &other) {
delete data_;
data_ = new std::string(*other.data_);
}
return *this;
}
const char* c_str() const { return data_->c_str(); }
private:
std::string* data_;
};
2.1 关键点说明
-
移动构造
- 直接转移
data_指针,避免了字符串拷贝。 - 将源对象的
data_置为nullptr,确保其析构时不再释放内部资源。
- 直接转移
-
拷贝构造
- 通过
new std::string(*other.data_)生成新的字符串,实现深拷贝。 - 这样每个对象都有独立的
std::string,互不影响。
- 通过
-
移动赋值
- 同移动构造,只需转移指针并清理源对象指针。
-
拷贝赋值
- 先
delete原有data_,再进行深拷贝,确保资源安全。
- 先
-
析构
- 默认析构会
delete data_,但若data_为nullptr,delete也安全。
- 默认析构会
3. 对比标准实现
标准库实现通常采用 std::shared_ptr<std::string> 或内部的 basic_string_view 结合引用计数,以进一步减轻内存分配压力。上例的核心思想与标准实现一致:
- 移动:仅转移内部指针或引用计数。
- 拷贝:在必要时深拷贝,保持对象独立。
4. 安全性与性能
- 线程安全:若使用
std::shared_ptr,引用计数操作是原子性的,线程安全;上例的裸指针不具备此特性,需要额外同步措施。 - 性能:移动操作为 O(1),拷贝操作为 O(n),其中 n 为路径字符串长度。
- 异常安全:所有分配都在构造或赋值中使用 RAII,异常抛出时资源得到正确释放。
5. 小结
通过上述实现,Path 对象既支持高效的移动语义,又能在需要时安全地深拷贝。实际项目中,可以根据需求选择使用裸指针、std::unique_ptr 或 std::shared_ptr 等智能指针,来平衡性能、线程安全与实现简洁性。C++ 17 的 std::filesystem::path 设计充分体现了这一理念,值得我们在自定义字符串包装器时参考借鉴。