[Effective Modern C++] 条款19笔记 - 为什么deleter的类型是std::unique_ptr类型的一部分,而不是std::shared_ptr的一部分?

为什么deleter的类型是std::unique_ptr类型的一部分,而不是std::shared_ptr的一部分?

std::unique_ptr<Widget, decltype(loggingDel) > upw(new Widget, loggingDel); 
std::shared_ptr<Widget> upw(new Widget, loggingDel);

这个问题涉及到 std::unique_ptrstd::shared_ptr 在 C++ 中设计上的一些关键区别,特别是在它们如何处理自定义删除器时的行为和接口。让我们深入理解为什么 std::unique_ptr 的类型包含删除器,而 std::shared_ptr 不需要这样做。

std::unique_ptr 的设计与删除器

std::unique_ptr 是一个轻量级的、独占所有权的智能指针。它的设计目的是简单和高效地管理动态分配的对象,确保对象在指针离开作用域时自动释放。以下是它与删除器相关的关键特性:

  1. 类型定义:

    • std::unique_ptr 的类型包括指向的对象类型(T)和删除器类型(D)。这种设计使得删除器类型是 std::unique_ptr 类型的一部分:
      std::unique_ptr<T, D>
      
    • 这样设计的原因是 std::unique_ptr 在编译时完全知道如何处理其管理的对象,包括如何销毁它。
    • 因此,std::unique_ptr 在对象销毁时可以直接调用删除器 D,不需要额外的存储来管理删除器的状态。
  2. 编译期特性:

    • 因为删除器是类型的一部分,所以 std::unique_ptr 可以在编译时确定其大小和行为。
    • 这使得 std::unique_ptr 在管理带有自定义删除器的对象时非常高效,因为删除器的开销在编译时就被固定了。
  3. 灵活性:

    • std::unique_ptr 可以支持无状态的(stateless)删除器,也可以支持有状态的(stateful)删除器。
    • 通过将删除器作为模板参数,std::unique_ptr 可以灵活地支持不同类型的删除器,而无需额外的运行时开销。

std::shared_ptr 的设计与删除器

std::shared_ptr 是一个更复杂的智能指针,设计用于共享所有权的场景。多个 std::shared_ptr 可以共享同一个对象,并在最后一个 std::shared_ptr 被销毁时自动释放对象。以下是它与删除器相关的关键特性:

  1. 类型定义:

    • std::shared_ptr 的类型定义中只包括指向的对象类型(T),而不包括删除器类型:
      std::shared_ptr<T>
      
    • 删除器在 std::shared_ptr 内部被处理为动态存储的部分,而不是类型的一部分。
  2. 动态行为:

    • std::shared_ptr 的核心是共享计数(reference count),它需要在运行时动态管理对象的生命周期。
    • 当创建 std::shared_ptr 时,可以传递一个删除器,但删除器不是 std::shared_ptr 类型的一部分。
    • 删除器被存储在 std::shared_ptr 内部的控制块(control block)中,控制块管理对象的引用计数和删除器。
    • 这种设计允许多个 std::shared_ptr 实例共享相同的对象,即使它们在不同的上下文中使用不同的删除器(尽管这种情况不常见)。
  3. 灵活性与复杂性:

    • 由于删除器是动态管理的,std::shared_ptr 可以更灵活地处理不同类型的删除器。
    • 但是,这也引入了更多的复杂性和潜在的运行时开销,因为控制块需要管理删除器的生命周期。

为什么 std::unique_ptr 的删除器是类型的一部分,而 std::shared_ptr 不是?

  • std::unique_ptr:

    • 设计目标是轻量级、高效的内存管理,它在编译时确定所有的行为,因此将删除器作为类型的一部分,使得它可以在最小的开销下管理对象。
    • 这种设计避免了额外的运行时存储和开销,使得 std::unique_ptr 尤其适合于不需要共享所有权的场景。
  • std::shared_ptr:

    • 设计目标是管理共享所有权,允许多个指针指向同一个对象,并在最后一个指针销毁时释放对象。
    • 由于 std::shared_ptr 必须动态管理引用计数和对象的生命周期,删除器被存储在控制块中,使得不同的 std::shared_ptr 实例可以共享同一个控制块,即使删除器不同。
    • 这种设计虽然引入了更多的复杂性和开销,但提供了更大的灵活性,允许在共享所有权的情况下安全地管理对象。

示例说明

以下是分别使用 std::unique_ptrstd::shared_ptr 的示例,展示了如何处理自定义删除器:

std::unique_ptr 示例:

#include <memory>
#include <iostream>

struct Widget {
    ~Widget() { std::cout << "Widget destroyed\n"; }
};

// 自定义删除器
auto loggingDel = [](Widget* p) {
    std::cout << "Deleting Widget with custom deleter\n";
    delete p;
};

int main() {
    // 删除器是类型的一部分
    std::unique_ptr<Widget, decltype(loggingDel)> upw(new Widget, loggingDel);

    // 使用 std::unique_ptr 时,类型包含了删除器的信息
    return 0;
}
  • 在这个示例中,std::unique_ptr<Widget, decltype(loggingDel)> 明确了删除器类型是 decltype(loggingDel)
  • 由于删除器是类型的一部分,std::unique_ptr 可以在编译时确定其行为和大小。

std::shared_ptr 示例:

#include <memory>
#include <iostream>

struct Widget {
    ~Widget() { std::cout << "Widget destroyed\n"; }
};

// 自定义删除器
auto loggingDel = [](Widget* p) {
    std::cout << "Deleting Widget with custom deleter\n";
    delete p;
};

int main() {
    // 删除器不在类型定义中,而是在实例化时指定
    std::shared_ptr<Widget> spw(new Widget, loggingDel);

    // 使用 std::shared_ptr 时,删除器是控制块的一部分,不是类型的一部分
    return 0;
}
  • 在这个示例中,std::shared_ptr<Widget> 类型只定义了对象类型 Widget,删除器在实例化时传递。
  • std::shared_ptr 动态地将删除器存储在控制块中,这样可以管理对象的生命周期,即使有多个共享所有权的实例。

总结

  • std::unique_ptr 将删除器作为类型的一部分,这使得它在管理对象时非常高效,具有最小的运行时开销。
  • std::shared_ptr 动态地管理删除器,这赋予了它更大的灵活性,以便处理共享所有权的复杂场景,尽管这增加了一些运行时开销。

这种设计上的区别使得 std::unique_ptr 更适合简单、非共享所有权的场景,而 std::shared_ptr 更适合复杂、需要共享所有权的场景。

posted @ 2024-06-21 16:59  围城chen  阅读(3)  评论(0编辑  收藏  举报