原文链接:
http://www.webkit.org/coding/RefPtr.html
历史
WebKit里的很多对象都是引用计数的。这些支持引用计数的类都包含ref和deref成员函数,分别用于增加和减少引用数。每次调用ref都必须匹配的调用deref。当deref被调用时,如果引用数变为1,这个对象就会被释放。WebKit里很多类通过继承RefCounted类,按照这个模式实现了引用计数。
回到2005年,我们发现了很多内存泄漏,特别是在编辑HTML模块的代码中,造成这个现象的原因是ref和deref调用不匹配。
我们需要使用智能指针来缓解这个问题。尽管如此,一些早期的经验告诉我们智能指针会导致更多的计数操作从而影响性能。比如,一个使用智能指针作为参数和返回值的函数,只是简单的传递和返回一个值就有可能造成二到四次引用计数的增加和减少,原因是这个对象被从一个智能指针转移到了另一个智能指针。因此我们需要寻找一个方案,既能使用智能指针,又能避免额外的计数操作。
我们从C++标准模板类auto_ptr上得到了提示。这些对象实现了一种专递“所有权”的模型。当你将一个auto_ptr赋值给另一个,原来的auto_ptr不再保持对实际对象的引用(由被赋值的auto_ptr继续对实际对象进行引用)。
Maciej Stachowiak制定了一对类模板,RefPtr和PassRefPtr,来实现WebCore里的引用计数计划。
原始指针
当我们讨论到智能指针比如RefPtr类模板时,我们使用术语“原始指针”来指代C++语言内置指针类型。这里是一个setter函数的范例,使用原始指针:
// example, not preferred style class Document { //... Title* m_title; } Document::Document() : m_title(0) { } 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来增加计数,调用deref来减少计数。RefPtr可以和所有提供了ref和deref成员函数的对象一起工作。这里是使用RefPtr重写的setter函数:
// example, not preferred style class Document { //... RefPtr<Title> m_title; } void Document::setTitle(Title* title) { m_title = title; }
使用RefPtr来进行拷贝可以使对象的引用计数自行增减
// example, not preferred style; should use RefCounted and adoptRef (see below) RefPtr<Node> createSpecialNode() { RefPtr<Node> a = new Node; a->setSpecial(true); return a; } RefPtr<Node> b = createSpecialNode();
为了实现上述目的,我们需要假定Node对象的引用计数开始于0(关于这一点后面有更详细的讨论)。当它被赋值给a,引用计数增加到1。创建返回值临时对象时,引用计数增加到2。a被销毁时,引用计数减小到1。当创建b(通过拷贝构造函数)时,引用计数增加到2。最后RefPtr<Node>临时对象被销毁,引用计数减小到1。
(如果编译器实现了return value optimization机制,可能可以避免创建返回值临时对象从而减少一轮引用计数的增加和减小)。
对引用计数的额外操作甚至可能大于函数本身的开销。解决方案就是PassRefPtr。
PassRefPtr
PassRefPtr类似于RefPtr,但有一点不同:你可以拷贝一个PassRefPtr,或者将一个PassRefPtr赋值给RefPtr/PassRefPtr,则原来的PassRefPtr中,对目标对象的引用将置为NULL,对象的引用计数不变。让我们再看一眼上述示例的新版本:
// example, not preferred style; should use RefCounted and adoptRef (see below) PassRefPtr<Node> createSpecialNode() { PassRefPtr<Node> a = new Node; a->setSpecial(true); return a; } RefPtr<Node> b = createSpecialNode();
Node对象的引用计数开始于0。当它被赋值给a,引用计数增加到1。创建返回值临时对象时,a对Node对象的引用置为NULL,由PassRefPtr<Node>临时对象引用Node对象。当b被创建后,临时对象对Node对象的引用被置为NULL,b维护着对Node对象的引用。
尽管如此,Safari团队发现当我们开始使用PassRefPtr时,将PassRefPtr对目标对象的引用置为NULL的操作可能引发一些问题:
// warning, will dereference a null pointer and will not work static RefPtr<Ring> g_oneRingToRuleThemAll; void finish(PassRefPtr<Ring> ring) { g_oneRingToRuleThemAll = ring; //... ring->wear(); }
由于ring已经不再引用实际的Ring对象了,当wear被调用时就会产生错误。为了防止这种情况,我们建议PassRefPtr只在作为函数参数和返回值的时候使用,将实际参数拷贝到RefPtr局部变量后在函数体里使用:
static RefPtr<Ring> g_oneRingToRuleThemAll; void finish(PassRefPtr<Ring> prpRing) { RefPtr<Ring> ring = prpRing; g_oneRingToRuleThemAll = ring; ... ring->wear(); }
PassRefPtr和RefPtr混合使用
我们建议除了函数参数和返回值使用PassRefPtr,除此之外在任何场合都尽量使用RefPtr。有时候会遇到需要转移RefPtr所有者的情况(类似PassRefPtr),RefPtr提供了一个release成员函数来完成这个工作。它将RefPtr对目标对象的引用置为NULL,同时构造一个PassRefPtr来维护对目标对象的引用,这个过程目标对象的引用计数不改变(相当于将RefPtr转化为PassRefPtr)
// example, not preferred style; should use RefCounted and adoptRef (see below) PassRefPtr<Node> createSpecialNode() { RefPtr<Node> a = new Node; a->setCreated(true); return a.release(); } RefPtr<Node> b = createSpecialNode();
这保持了PassRefPtr的效率,同时减少了因为PassRefPtr的语义而可能造成的问题。
与原始指针混合使用
当函数需要传入原始指针,我们通过RefPtr的get成员函数来获取它:
printNode(stderr, a.get());
尽管如此,很多操作可以直接通过PassRefPtr和RefPtr来进行,并不需要显式调用get函数来恢复为原始指针:
RefPtr<Node> a = createSpecialNode(); Node* b = getOrdinaryNode(); // the * operator *a = value; // the -> operator a->clear(); // null check in an if statement if (a) log("not empty"); // the ! operator if (!a) log("empty"); // the == and != operators, mixing with raw pointers if (a == b) log("equal"); if (a != b) log("not equal"); // some type casts RefPtr<DerivedNode> d = static_pointer_cast<DerivedNode>(a);
通常,PassRefPtr和RefPtr遵循一个简单规则:它们始终平衡ref和deref的调用,保障程序员不会遗失一个deref。但有时候我们已经有了一个包含特定引用计数的原始指针,只是想转移到RefPtr中进行维护(意味着RefPtr对ref和deref的调用不再完全匹配),可以使用adoptRef函数来实现这个需求:
// warning, requires a pointer that already has a ref RefPtr<Node> node = adoptRef(rawNodePointer);
(原文中提到的leakRef已经从最新的wtf代码中删除,故此处不再翻译)
RefPtr和新建对象
我们上面讨论到的范例,都提到了对象的引用计数开始与0。尽管如此,从效率和简便的角度出发,RefCounted从来不会将引用计数置为0。对象被创建时,引用计数实际被初始化为1。良好的编程习惯是创建完这些对象后立即将其放入RefPtr中再进行使用,由RefPtr进行管理,避免因为疏忽而忘记调用deref。这意味着任何new出来的对象应立即调用adoptRef。在WebCore中我们用create函数来代替new:
// preferred style PassRefPtr<Node> Node::create() { return adoptRef(new Node); } RefPtr<Node> e = Node::create();
按照adoptRef和PassRefPtr的实现方式,这是一种非常高效的风格。对象开始时引用计数为1,创建过程中没有对引用计数进行过操作。
// preferred style PassRefPtr<Node> createSpecialNode() { RefPtr<Node> a = Node::create(); a->setCreated(true); return a.release(); } RefPtr<Node> b = createSpecialNode();
通过调用Node::create里的adoptRef,Node对象被放置到PassRefPtr中,接着传递给a,release后再传递给b,整个过程引用计数没有任何改变。
RefCounted类实现了一个运行时检查:创建对象后,在调用ref和deref前,如果没有调用过adoptRef,则程序会产生一个断言失败。
指导方针
我们总结了一些指导方针,用于在WebKit代码中使用RefPtr和PassRefPtr:
局部变量
- 如果可以人为保证所有权和生命周期,局部变量可以为原始指针。
- 如果需要代码自动控制所有权和生命周期,局部变量应该用RefPtr。
- 局部变量绝不应该是PassRefPtr。
数据成员
- 如果可以人为保证所有权和生命周期,数据成员可以为原始指针。
- 如果需要类自动控制所有权和生命周期,数据成员应该用RefPtr。
- 数据成员绝不应该是PassRefPtr。
函数参数
- 如果函数并不获取一个对象的所有权,那么参数应该应该是原始指针。
- 如果函数需要获取一个对象的所有权,则参数应该是PassRefPtr。这包括了大部分setter函数。除非参数的使用非常简单,否则应该在函数一开始就将参数转化为RefPtr,这种情况下这类参数通常以prp为前缀命名。
函数返回值
- 如果函数返回值是一个对象,但其所有权并没有被转移,则返回值应该是一个原始指针。这包括了大部分getter函数。
- 如果函数返回值是一个新建对象,或者由于某种原因所有权被转移了,则返回值应该是个PassRefPtr。由于局部变量一般是RefPtr,所以通常会在返回语句里调用release函数将RefPtr转化为PassRefPtr。
新建对象
- 为了能通过智能指针自动进行所有的引用计数操作,新建对象应该在创建后尽可能快的放入RefPtr里。
- 对于继承了RefCounted的类对象,上述动作应该通过adoptRef函数进行。
- 好的编程方式是私有化构造函数,同时提供create函数来返回一个PassRefPtr。