条款 18 - 使用 std::unique_ptr 管理具备专属所有权的资源

ZaynPei Lv6

进入现代C++, 传统的指针由于容易出现资源泄露等缺点, 智能指针成为了管理资源的重要工具. C++11 中共有四种智能指针:std::auto _ptr, std::unique_ptr, std::shared_ptr 和 std: :weak_ptr。所有这些智能指针都是为管理动态分配对象的生命期而设计的,其中, std::unique_ptr 是一种独占式智能指针, 它确保在任何时候只有一个指针指向资源. 这使得 std::unique_ptr 成为了管理具备专属所有权的资源的理想选择.

而且在一般情况下, std::unique_ptr 应该是智能指针的默认首选, 它在效率上也几乎与裸指针无异

std::auto_ptr 是从 C++98 中残留下来的弃用特性, 现在已经基本被std::unique_ptr 所替代.

专属所有权与移动语义

std::unique_ptr 体现了“专属所有权”的概念,即一个非空的 std::unique_ptr 总是拥有其所指向的资源 。任何时刻,资源都只有一个所有者。这确保了资源的安全释放,避免了内存泄漏和悬空指针的问题。

轻量且高效:在默认情况下(即使用 delete 作为删除器),std::unique_ptr 的尺寸与裸指针完全相同,并且其操作(如解引用)的执行效率也与裸指针相同 。这使得它即使在对性能和内存要求极为苛刻的场景下也同样适用 。

移动专属 (Move-Only):std::unique_ptr 不允许复制, 如果你尝试复制一个 std::unique_ptr,代码将无法通过编译。这是为了保证所有权的唯一性。因此要转移资源的所有权,只能且必须使用 std::move 。当一个 std::unique_ptr 被移动后,源指针将被置为 nullptr 。

自动资源管理:当一个 std::unique_ptr 被销毁时(例如离开作用域),它会自动销毁其所拥有的资源 。默认情况下,它通过在其内部的裸指针上调用 delete 来完成这一操作 。

典型用例:工厂函数

std::unique_ptr 最常见的用法之一,是作为工厂函数的返回类型 。工厂函数通常在堆上创建一个对象并返回一个指向它的指针,而调用者则负责该对象的生命周期, 这与 std::unique_ptr 的专属所有权语义完美契合。工厂函数通过返回一个 std::unique_ptr,清晰地将新创建的对象的所有权转移给调用方 。

1
2
3
4
5
6
7
8
9
10
11
12
// 假设 Investment 是一个多态基类
class Investment { ... };

// 工厂函数返回一个 unique_ptr,将所有权转移给调用者
template<typename... Ts>
std::unique_ptr<Investment> makeInvestment(Ts&&... params);

{
// 调用工厂函数,获取资源的所有权
auto pInvestment = makeInvestment(arguments);

} // pInvestment 在此处离开作用域,其管理的 Investment 对象被自动销毁
调用方的代码将变得非常安全和简洁,因为当 pInvestment 离开作用域时,std::unique_ptr 的析构函数会自动确保资源被正确释放,即使在发生异常时也是如此 。

自定义删除器

std::unique_ptr 不仅限于使用 delete 来释放资源,它还支持自定义删除器 (custom deleter) 。删除器可以是任意函数或函数对象(包括 lambda 表达式),用于在 std::unique_ptr 销毁时执行特定的清理操作。自定义删除器的类型是 std::unique_ptr 模板的第二个参数 。

1
2
3
4
5
6
7
8
9
// 自定义删除器,在删除前先写入日志
auto delInvmt = [](Investment* pInvestment) {
makeLogEntry(pInvestment);
delete pInvestment;
};

// 在 unique_ptr 的类型中指定删除器的类型
std::unique_ptr<Investment, decltype(delInvmt)>
pInv(nullptr, delInvmt);
需要注意的是, 自定义删除器可能会增加 std::unique_ptr 的尺寸 。

函数指针删除器:如果删除器是一个函数指针,std::unique_ptr 的尺寸通常会从一个字长(裸指针的大小)增加到两个字长(一个用于裸指针,一个用于函数指针)。

函数对象删除器(包括 lambda):如果删除器是一个函数对象,其尺寸取决于该函数对象中存储了多少状态。如果是一个无捕获的 lambda,它不包含任何状态,编译器通常可以将其完全优化掉,此时 std::unique_ptr 的尺寸不会增加,仍然和一个裸指针一样大 。

因此,当需要自定义删除器时,使用无捕获的 lambda 是比使用函数指针更高效的选择 。

std::unique_ptr 的两种形式:单个对象与数组

std::unique_ptr 提供两种形式:一种用于单个对象,一种用于数组 。

  • std::unique_ptr:用于单个对象。它提供了 operator* 和 operator->。

  • std::unique_ptr<T[]>:用于数组。它提供了 operator[] 索引运算符,但不提供 * 和 -> 。

不过, 在现代 C++ 中,std::array、std::vector 和 std::string 等标准容器几乎总是比裸数组更好的选择,因此 std::unique_ptr<T[]> 的使用场景非常有限 。

转换为 std::shared_ptr

std::unique_ptr 的一个非常吸引人的特性是,它可以方便且高效地转换为 std::shared_ptr。

并且通常来说, std::unique_ptr 是构建 std::shared_ptr 的完美来源。这使得 std::unique_ptr 成为工厂函数的理想返回类型。工厂函数返回一个高效的 std::unique_ptr,将专属所有权交给调用者。如果调用者后续需要共享这个资源,他们可以自行决定将其转换为 std::shared_ptr,从而将所有权模型从专属升级为共享 。