RefPtr 基础

历史

WebKit 中有许多对象是引用计数的。这种模式是类拥有名为 refderef 的成员函数,用于增加和减少引用计数。当对引用计数为 1 的对象调用 deref 函数时,该对象将被删除。WebKit 中的许多类通过派生自 RefCounted 类模板来实现此模式。

早在 2005 年,我们发现存在许多内存泄漏,尤其是在 HTML 编辑代码中,这是由于滥用 refderef 调用造成的。我们决定使用智能指针来缓解这个问题。早期实验表明,智能指针导致额外的引用计数操作,从而损害了性能。例如,对于一个接受智能指针作为参数并返回相同智能指针作为返回值的函数,在对象从一个智能指针移动到另一个智能指针时,传递参数和返回值会将引用计数增加然后减少两到四次。

我们在 2005 年通过一组智能指针类模板解决了这个问题。C++11 中引入的 C++ 移动语义使得简化这些类模板成为可能,而不会重新引入引用计数波动。

后来,在 2013 年,我们注意到对指针的普遍使用,特别是智能指针的使用,导致了大量的空检查和关于什么可以为空的不确定性。我们开始在 WebKit 代码中尽可能使用引用而不是指针。

Maciej Stachowiak 创建了类模板 RefPtr,它实现了 WebKit 的侵入式引用计数,此后我们对其进行了调整,使其能够很好地与移动语义配合使用。Andreas Kling 创建了一个相关的类模板 Ref,它与 RefPtr 协同工作,并在不需要空指针的上下文中处理引用计数对象时提供清晰度和更高的效率。

裸指针

在讨论诸如 RefPtr 类模板之类的智能指针时,我们使用“裸指针”一词来指代 C++ 语言的内置指针类型。以下是使用裸指针编写的规范 setter 函数示例:

// example, not preferred style

class Document {
    ...
    Title* m_title { nullptr };
}

Document::~Document()
{
    if (m_title)
        m_title->deref();
}

void Document::setTitle(Title* title)
{
    if (title)
        title->ref();
    if (m_title)
        m_title->deref();
    m_title = title;
}

RefPtr

RefPtr 是一个智能指针类模板,它对传入值调用 ref,对传出值调用 derefRefPtr 适用于任何同时具有 refderef 成员函数的对象。以下是使用 RefPtr 编写的 setter 函数示例:

// example, not preferred style

class Document {
    ...
    RefPtr<Title> m_title;
}

void Document::setTitle(Title* title)
{
    m_title = title;
}

获取引用计数参数所有权的函数可能导致引用计数波动。

// example, not preferred style

RefPtr<Title> untitledTitle = titleFactory().createUniqueUntitledTitle();

document.setTitle(untitledTitle);

标题的引用计数从 1 开始。setTitle 函数将其存储在数据成员中,引用计数增加到 2。然后局部变量 untitledTitle 超出作用域,引用计数又减少回 1。

定义一个获取对象所有权的函数的方法是使用右值引用。

// preferred style

class Document {
    ...
    RefPtr<Title> m_title;
}

void Document::setTitle(RefPtr<Title>&& title)
{
    m_title = WTF::move(title);
}

…

RefPtr<Title> untitledTitle = titleFactory().createUniqueUntitledTitle();

document.setTitle(WTF::move(untitledTitle));

标题以引用计数 1 的方式完整地进入数据成员;它从未被增加或减少。

请注意使用 WTF::move 而不是 std::move。WTF 版本增加了一些编译时检查来捕获常见错误,并且应该在整个 WebKit 项目中替代 std::move 使用。

Ref

Ref 类似于 RefPtr,不同之处在于它表现得像一个引用而不是指针;它没有空值。

Ref 特别适用于返回值;确保新创建的对象永不为空通常很简单。

// preferred style

Ref<Title> TitleFactory::createUniqueUntitledTitle()
{
    return createTitle("untitled " + m_nextAvailableUntitledNumber++);
}

使用 Ref 有助于向调用者表明此函数永远不会返回空。

与裸指针混合使用

RefPtr 类与裸指针的混合使用方式与 C++ 标准库中的智能指针(例如 std::unique_ptr)类似。

当使用 RefPtr 调用一个接受裸指针的函数时,请使用 get 函数。

printNode(stderr, a.get());

对于 Refget 函数会生成一个裸引用,而 ptr 函数会生成一个裸指针。

printString(stderr, a.get().caption());
printNode(stderr, a.ptr());

许多操作可以直接在 RefPtr 上完成,而无需显式调用 get

Ref<Node> a = createSpecialNode();
RefPtr<Node> b = findNode();
Node* c = getOrdinaryNode();

// the * operator
*b = value;

// the -> operator
a->clear();
b->clear();

// null check in an if statement
if (b)
    log("not empty");

// the ! operator
if (!b)
    log("empty");

// the == and != operators, mixing with raw pointers
if (b == c)
    log("equal");
if (b != c)
    log("not equal");

// some type casts
RefPtr<DerivedNode> d = static_pointer_cast<DerivedNode>(d);

通常,RefPtr 遵循一个简单规则;它始终平衡 refderef 调用,确保程序员不会遗漏 deref。但在我们从裸指针开始,已经有引用计数,并且想要转移所有权的情况下,应该使用 adoptRef 函数。

// warning, requires a pointer that already has a ref
RefPtr<Node> node = adoptRef(rawNodePointer);

在极少数情况下,如果我们需要将 RefPtr 转移到裸指针而不改变引用计数,请使用 leakRef 函数。

// warning, results in a pointer that must get an explicit deref
RefPtr<Node> node = createSpecialNode();
Node* rawNodePointer = node.leakRef();

RefPtr 与新对象

使用 RefCounted 类模板的类的新对象以引用计数 1 创建。最佳的编程习惯是直接将此类对象放入 Ref 中,以避免在完成使用后忘记对对象进行 deref 操作。这意味着任何对此类对象调用 new 的人应立即调用 adoptRef。在 WebKit 中,我们为这些类使用名为 create 的函数,而不是直接调用 new。

// preferred style

Ref<Node> Node::create()
{
    return adoptRef(*new Node);
}

Ref<Node> e = Node::create();

由于 adoptRef 的实现方式,这是一种高效的习惯用法。对象以引用计数 1 开始,并且没有生成任何代码来检查或修改引用计数。

// preferred style

Ref<Node> createSpecialNode()
{
    Ref<Node> a = Node::create();
    a->setCreated(true);
    return a;
}

Ref<Node> b = createSpecialNode();

节点对象通过 Node::create 内部对 adoptRef 的调用被放入 Ref,然后传递给 a 并传递给 b,所有这些操作都没有触及引用计数。

RefCounted 类实现了一个运行时检查,因此如果我们创建对象并在未首先调用 adoptRef 的情况下调用 refderef,我们将得到断言失败。

指南

我们制定了这些指南,用于在 WebKit 代码中使用 RefPtrRef

局部变量

  • 如果所有权和生命周期得到保证,局部变量可以是裸引用或裸指针。
  • 如果代码需要持有所有权或保证生命周期,局部变量应该是一个 Ref,如果它可以为空,则应该是一个 RefPtr

数据成员

  • 如果所有权和生命周期得到保证,数据成员可以是裸引用或裸指针。
  • 如果类需要持有所有权或保证生命周期,则数据成员应该是一个 RefRefPtr

函数参数

  • 如果函数不获取对象的所有权,则参数应该是裸引用或裸指针。
  • 如果函数确实获取对象的所有权,则参数应为 Ref&&RefPtr&&。这包括许多 setter 函数。

函数结果

  • 如果函数的返回值是一个对象,但所有权并未转移,则结果应为裸引用或裸指针。这包括大多数 getter 函数。
  • 如果函数的返回值是一个新对象或因任何其他原因转移所有权,则结果应为 RefRefPtr

新对象

  • 新对象应在创建后尽快放入 Ref 中,以允许智能指针自动完成所有引用计数。
  • 对于 RefCounted 对象,上述操作应通过 adoptRef 函数完成。
  • 最佳习惯用法是使用私有构造函数和返回 Ref 的公共 create 函数。

常见陷阱

PassRefPtr

在 C++11 之前在 WebKit 上工作的程序员都熟悉一个名为 PassRefPtr(即将更名为 DeprecatedPassRefPtr)的类模板,你会在较旧的 WebKit 代码中看到它。

  • 任何 PassRefPtr 类型的函数结果或局部变量都应替换为 RefPtr 或 Ref 类型。
  • 任何 PassRefPtr 类型的参数都应替换为 RefPtr&& 或 Ref&& 类型。
  • 调用 RefPtr::release 将 RefPtr 转换为 PassRefPtr 的代码应改为调用 WTF::move。

常见错误

  • 在参数本应是裸引用或裸指针的情况下,为其指定 Ref、RefPtr、Ref&& 或 RefPtr&& 类型。一个有时会获取所有权的函数使用裸引用或裸指针也能正常工作。当传递所有权是函数主要使用方式且需要优化的场景时,右值引用形式是合适的。并非所有 setter 都需要接受右值引用。
  • 忘记调用 WTF::move 可能导致不必要的引用计数波动。

改进本文档

我们应该添加本文档未涵盖的任何常见问题的答案。本文档还可以涵盖以下一个或多个主题。

  • copyRef
  • releaseNonNull
  • 当这些对象存储在向量和哈希映射等集合中时,其工作原理。
  • WTF::move 何时需要和何时不需要的更好解释。
  • “保护器”惯用法,即使用局部 Ref 变量来保持对象存活。
  • 使用 TreeShared 编程的风险。(或者在我们将 TreeShared 合并到 Node 后,使用 Node 编程的风险)。
  • 我们希望消除 TreeShared,并让节点持有对其第一个子节点和下一个兄弟节点的引用。
  • 我们如何将引用计数与垃圾回收混合使用,以实现 DOM 以及 JavaScript 和 Objective-C DOM 绑定。
  • WebKit 侵入式引用计数与外部引用计数(例如 std::shared_ptr 中的)方案的比较。
  • std::unique_ptrstd::make_unique 的使用指南。
  • RetainPtr 类模板。

如果您对上述内容有任何评论,或有其他关于提高清晰度、范围或展示的建议,请发送邮件至WebKit 邮件列表