歷史
在WebKit中,很多對象採用了引用計數。使用的模式是,通過類成員ref和deref來增加和減少引用計數。每一個ref調用必須有一個相應的deref。當一個對象的引用計數是1時,在其上調用deref將會刪除該對象。在WebKit中,很多類是通過繼承RefCounted類模板來實現這種模式的。
早在2005年,我們發現有太多內存泄露了,尤其是在HTML編輯代碼中,這是由於對ref和deref的誤用造成的。
我們想通過使用智能指針來改善內存泄露問題。然而,早期的一些實驗證實智能指針會導致額外的引用計數操作,這會損害性能。例如,對於一個帶有一個智能指針參數,並返回同一個智能指針的函數而言,當我們調用該函數把一個對象從一個智能指針移動到另一個智能指針時,傳遞參數和返回值就會遞增和遞減引用計數2-4次。所以我們就想找一種方法,既可以使用智能指針,又能夠避免引用計數操作的開銷。
解決的靈感來自C++的標準類模板auto_ptr。對auto_ptr賦值就是轉移所有權。當你把一個auto_ptr賦值個另一個時,原來的auto_ptr變爲0。
Maciej Stachowiak發明了一對類模板,RefPtr 和 PassRefPtr,它們實現了WebCore的侵入式引用計數方案。
原始指針
在討論智能指針,比如RefPtr類模板時,我們使用術語raw pointer來指稱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是一個簡單的智能指針類,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,然後createSpecialNode的返回值被銷燬 ,引用計數又減少到1。
如果編譯器實現了返回值優化,引用計數增加和減少的次數可能會減少。
如果參數和返回值都是智能指針,引用計數附帶的開銷就更大了。解決這一個問題的方法是使用PassRefPtr
PassRefPtr
PassRefPtr與RefPtr有一點不同,當你拷貝一個PassRefPtr或者把一個PassRefPtr的值賦給一個RefPtr或另一個PassRefPtr時,原來的指針值被設置爲0; 該操作不會改變引用計數的值。我們來看前一個例子的新版本:
// 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. 當返回值PassPtrRef被創建時,a被設置0. 當b被創建時,返回值又被設爲0.
然而,當Safari團隊開始使用PassRefPtr進行編程時,我們發現,當賦值給另一個變量時,原來的指針變爲0這條規則很容易導致錯誤。
// 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();
}
wear被調用時,ring已經變爲0了。爲了避免這種情況,我們推薦只在函數參數和返回類型中使用PassRefPtr,並把函數參數拷貝到一個RefPtr中使用。
static RefPtr <Ring> g_oneRingToRuleThemAll;
void finish (PassRefPtr<Ring> prpRing)
{
RefPtr<Ring> ring = prpRing;
g_oneRingToRuleThemAll = ring ;
...
ring->wear();
}
混合使用RefPtr和PassRefPtr
我們推薦在除了傳遞參數或從一個函數返回值的所有場合使用RefPtr,但有時,你有一個RefPtr,而且希望像PassRefPtr一樣可以轉移它的所有權。RefPtr有一個叫做release的成員函數可以做到。它把原來的RefPtr設爲0,並構造一個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的效率,同時也降低了它相對複雜的語義可能引起問題的機率。
混合使用raw pointers
當使用一個RefPtr調用一個需要一個raw pointer的函數時,使用get函數。
printNode(stderr , a.get());
不過,許多操作其實可以直接使用RefPtr和PassRefPtr直接完成,而不用借組一個顯示的get調用。這是通過RefPtr和PassRefPtr重載的一些操作完成的。
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);
正常情況下,RefPtr和PassRefPtr都執行一條簡單的規則;通過ref和deref調用達到平衡,這是建立在用戶不會忘記調用deref的基礎上的。不過有這種情況,我們有一個raw pointer,它已經有一個引用計數,而且我們想要轉移它的所有權,這時就可以使用adoptRef函數。
// warning, requires a pointer that already has a ref
RefPtr<Node> node = adoptRef(rawNodePointer);
PassRefPtr提供了函數leakRef,它可以再不改變引用計數的情況下,把一個RefPtr轉移給一個raw pointer。
// warning, results in a pointer that must get an explicit deref
RefPtr<Node> node = createSpecialNode();
Node* rawNodePointer = node.release ().leakRef();
因爲leakRef很少用到,所以只在PassRefPtr類中提供,所以,需要先調用release,在調用leakRef
上面提到三個函數,比較容易混淆,這裏總結一下
release: 使RefPtr可以像PassRefPtr一樣轉移所有權,它是RefPtr<T>的一個成員函數,實現如下
PassRefPtr<T > RefPtr<T >::release() { PassRefPtr<T > tmp = adoptRef( m_ptr); m_ptr = nullptr; return tmp ; }
adoptRef: 把一個raw pointer轉移給一個PassRefPtr,它是一個全局函數
template<typename T> inline PassRefPtr< T> adoptRef (T* p)
{
adopted(p );
return PassRefPtr <T>(p, PassRefPtr< T>::Adopt );
}
leakRef:可以實現把一個RefPtr轉移給一個raw pointer,它是PassRefPtr的一個成員函數,實現如下
template< typename T > inline T* PassRefPtr< T>::leakRef () const
{
T* ptr = m_ptr;
m_ptr = nullptr ;
return ptr ;
}
RefPtr有這樣兩個操作
template< typename U > RefPtr(const PassRefPtr< U>&);
template<typename U> RefPtr(PassRef< U>);
RefPtr和新對象
在前面我們提到引用計數爲0的對象。然而,出於效率和簡單性考慮,RefCounted類的引用計數根本不會爲0。對象被創建時,引用計數是1. 最好的編程習慣是立刻把新創建的對象放進一個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();
這些改進措施目的:自動管理資源,改善內存泄露情況。智能指針是通過引用計數的方式實現的,所謂魚和熊掌不可兼得,這必然帶來引用計數操作開銷問題。因此要考慮儘量減少這樣的開銷。