coding, photo, plant and demo

*RefPtrとPassRefPtrの基礎

tech 20120205 234643
RefPtr and PassRefPtr Basics
Darin Adler
Version 4, 2010-08-27
を訳してみた。

WebKitの中を見ると必ず出くわすクラステンプレートで、要はboostのintrusive_ptrと同じようなスマートポインタなんだけど、真面目に読んだ事がなかったので折角だから拙訳だけどここにメモしておく。

歴史

WebKitの多くのオブジェクトは参照カウンタを持つ。このパターンでは、クラスは参照カウンタを上下するref,derefメンバ関数を持つ。refとderefの呼び出しは一対一で対応する必要があり、参照カウントが1のときにderefが呼ばれると、オブジェクトは削除される。WebKitの多くのクラスはRefCountedクラステンプレートを基底としてこのパターンが実装されている。

2005年頃、私たちは大量のメモリーリークがあることを発見した。特にHTML editingコード(訳注:parser?dom周り?)における、refとderefの呼び出し間違いによるものだ。

この問題を軽減するために私たちはスマートポインタを使おうとした。しかし、初期の実験ではスマートポインタは参照カウンタの操作が増えてパフォーマンスが低下するであろうことを示した。例えば、ある関数がただスマートポインタの引数を渡しスマートポインタを返すだけで、スマートポインタを移すのと同じように参照カウンタの操作が2~4回必要となる。そこで私たちはスマートポインタを使いつつ参照カウンタの過剰な変更を抑制する方法を探した。

解決の糸口はC++標準クラステンプレートのauto_ptrだった。これは所有権の移動を代入によって行うというモデルだ。あるauto_ptrから他へ代入すると、その提供元のauto_ptrは0になる。

Maciej StachowiakはRefPtrとPassRefPtrというクラステンプレートのペアを考案した。WebCoreのintrusive参照カウンタの構想を実装したものだ。

生ポインタ

RefPtrのようなスマートポインタを議論するときに、C++の組込のポインタ型を生ポインタという呼ぶことにする。ここに生ポインタを使って書いた、標準的なセッター関数を示す。
(訳注:Titleがintrusiveな参照カウンタを持つクラス。こういった生ポインタを使った書き方ではrefやderefを忘れてしまいメモリリークやdanglingが起きやすい。そこで今回述べるクラステンプレートによるスマートポインタが必要となる)

  1. 1// 例、ただし好ましくない書き方
  2. 2
  3. 3class Document {
  4. 4 ...
  5. 5 Title* m_title;
  6. 6}
  7. 7
  8. 8Document::Document()
  9. 9 : m_title(0)
  10. 10{
  11. 11}
  12. 12
  13. 13Document::~Document()
  14. 14{
  15. 15 if (m_title)
  16. 16 m_title->deref();
  17. 17}
  18. 18
  19. 19void Document::setTitle(Title* title)
  20. 20{
  21. 21 if (title)
  22. 22 title->ref();
  23. 23 if (m_title)
  24. 24 m_title->deref();
  25. 25 m_title = title;
  26. 26}

RefPtr

RefPtrは単純なスマートポインタクラスで、値が入ってきたらrefを呼び、出て行くときにderefを呼ぶ。RefPtrはrefとderefメンバー関数の両方を持つオブジェクト上で動く。ここでRefPtrを使ったセッター関数の例を示す。
  1. 1// 例、ただし好ましくない書き方
  2. 2
  3. 3class Document {
  4. 4 ...
  5. 5 RefPtr<Title> m_title;
  6. 6}
  7. 7
  8. 8void Document::setTitle(Title* title)
  9. 9{
  10. 10 m_title = title;
  11. 11}

しかしRefPtr単独で使うことは参照カウンタの過剰な変更につながる。
  1. 1// 例、ただし好ましくない書き方。RefCountedとadoptRef(後述)を使うべき
  2. 2
  3. 3RefPtr<Node> createSpecialNode()
  4. 4{
  5. 5 RefPtr<Node> a = new Node;
  6. 6 a->setSpecial(true);
  7. 7 return a;
  8. 8}
  9. 9
  10. 10RefPtr<Node> b = createSpecialNode();
ここでNodeオブジェクトの参照カウンタは0から始まるとする。それがaに代入されるとき、参照カウンタは1になる。そして、返り値を作るために参照カウンタは2になり、aが破壊されるときに1に戻る。bが作られて代入されることで参照カウンタは2に増え、返り値が破壊されるときに1に戻る。(訳注:オブジェクトを生成して返すだけなのに、5回も参照カウンタが変更されてしまう)
(もしコンパイラが返り値の最適化を行うなら、参照カウンタの増減は1ずつ減るかもしれない)

参照カウンタの過剰な変更によるオーバーヘッドは、関数の引数と返り値の両方が関連しているとさらに増大する。それを解決するのがPassRefPtrだ。

PassRefPtr

PassRefPtrはRefPtrのようで違う。PassRefPtrへコピーするかPassRefPtrからRefPtrか別のPassRefPtrへ代入するとき、元々のポインタの値は0になる。この操作は参照カウントの変更を伴わない(訳注:これがauto_ptrと同じような挙動)。これを使った新しい例を見てみよう。

  1. 1// 例、ただし好ましくない書き方。RefCountedとadoptRef(後述)を使うべき
  2. 2
  3. 3PassRefPtr<Node> createSpecialNode()
  4. 4{
  5. 5 PassRefPtr<Node> a = new Node;
  6. 6 a->setSpecial(true);
  7. 7 return a;
  8. 8}
  9. 9
  10. 10RefPtr<Node> b = createSpecialNode();
Nodeオブジェクトは参照カウンタ0から始まる。aへ代入すると、参照カウンタは1に増加する。そしてaから返り値のPassRefPtrへ、さらにそこからbへ、とNodeオブジェクトを移している間、参照カウンタに変化はない。

しかしSafariチームがPassRefPtrを使ってプログラミングを始めたときに気づいたように、この他へ代入する際に0になるというルールは間違いを生みやすい。

  1. 1// 注意、参照先がnullになり動かない
  2. 2static RefPtr<Ring> g_oneRingToRuleThemAll;
  3. 3
  4. 4void finish(PassRefPtr<Ring> ring)
  5. 5{
  6. 6 g_oneRingToRuleThemAll = ring;
  7. 7 ...
  8. 8 ring->wear();
  9. 9}
wearが呼ばれるとき、ringは既に0だ。これを防ぐため、PassRefPtrを関数の引数と返り値の型にしか使わず、ローカル変数のRefPtrコピーして使うことを推奨する。

  1. 1static RefPtr<Ring> g_oneRingToRuleThemAll;
  2. 2
  3. 3void finish(PassRefPtr<Ring> prpRing)
  4. 4{
  5. 5 RefPtr<Ring> ring = prpRing;
  6. 6 g_oneRingToRuleThemAll = ring;
  7. 7 ...
  8. 8 ring->wear();
  9. 9}

RefPtrとPassRefPtrの混合

前述で引数と返り値を除く全ての場合でRefPtrを使うことを推奨したため、RefPtrから所有権をPassRefPtrのように移したいときも出てくるだろう。RefPtrにはそういったトリックを行うためのreleaseというメンバ関数がある。参照カウントの変更なく、元のRefPtrを0にしてPassRefPtrを生成する。

  1. 1// 例、ただし好ましくない書き方。RefCountedとadoptRef(後述)を使うべき
  2. 2
  3. 3PassRefPtr<Node> createSpecialNode()
  4. 4{
  5. 5 RefPtr<Node> a = new Node;
  6. 6 a->setCreated(true);
  7. 7 return a.release();
  8. 8}
  9. 9
  10. 10RefPtr<Node> b = createSpecialNode();
これはPassRefPtrの効果を保ちつつ、トリッキーな動作による問題が起きる可能性を減らす。

生ポインタとの混合

RefPtrを使いながら、生ポインタを必要とする関数を呼ぶ際にはget関数を使う。
  1. 1printNode(stderr, a.get());

しかし、多くの操作は明示的にgetを呼ばずとも、RefPtrやPassRefPtrを直接使えば済む。
  1. 1RefPtr<Node> a = createSpecialNode();
  2. 2Node* b = getOrdinaryNode();
  3. 3
  4. 4// the * operator
  5. 5 *a = value;
  6. 6
  7. 7// the -> operator
  8. 8a->clear();
  9. 9
  10. 10// null check in an if statement
  11. 11if (a)
  12. 12 log("not empty");
  13. 13
  14. 14// the ! operator
  15. 15if (!a)
  16. 16 log("empty");
  17. 17
  18. 18// the == and != operators, mixing with raw pointers
  19. 19if (a == b)
  20. 20 log("equal");
  21. 21if (a != b)
  22. 22 log("not equal");
  23. 23
  24. 24// some type casts
  25. 25RefPtr<DerivedNode> d = static_pointer_cast<DerivedNode>(a);

通常RefPtrとPassRefPtrは、プログラマがderefを間違えないよう保証して「refとderefが常に均衡する」という単純なルールを強制する。しかし、生ポインタを使う場合において、それが既に参照カウントを持ち所有権を移転したい場合は、adoptRef関数を使うべきだ。
  1. 1// 警告、既に参照を持ったポインタを要求する
  2. 2RefPtr<Node> node = adoptRef(rawNodePointer);

RefPtrから参照カウントを変えずに生ポインタへ移すときのため、PassRefPtrはleakRef関数を提供する。
  1. 1// 警告、ポインタになった結果、明示的にderefを呼ばなければならない。
  2. 2RefPtr<Node> node = createSpecialNode();
  3. 3Node* rawNodePointer = node.release().leakRef();
leakRefは滅多に使われないから、PassRefPtrクラスでしか実装されていない。だからreleaseを呼んでからleakを呼ぶ必要がある。

RefPtrと新しいオブジェクト

この議論の例においては、オブジェクトは参照カウントは0から始まるものとした。しかし効率と簡単のため、実際のRefCountedクラスはそうではなく、オブジェクトは生成時に1の参照カウントを持つ。このときベストなプログラミング表現は、derefを絶対に忘れないように生成後に即RefPtrへオブジェクトを入れることだ。これはオブジェクトをnewしたら即adoptRefを呼ぶことを意味する(訳注:ポインタをPassRefPtrへ渡したら通常はrefを1回呼ぶが、adoptRefはrefを呼ばずにPassRefPtrを生成する)。そこでWebCoreにおいてはcreateと呼ばれる関数を直接newを呼ぶ代わりに使う。
  1. 1// 好ましい書き方
  2. 2
  3. 3PassRefPtr<Node> Node::create()
  4. 4{
  5. 5 return adoptRef(new Node);
  6. 6}
  7. 7
  8. 8RefPtr<Node> e = Node::create();
adoptRefとPassRefPtrが実装されたことで効率的な書き方ができる。初めから参照カウントを1とすることで参照カウントの操作なく、オブジェクトの生成からRefPtrへの代入までができる。

  1. 1// 好ましい書き方
  2. 2
  3. 3PassRefPtr<Node> createSpecialNode()
  4. 4{
  5. 5 RefPtr<Node> a = Node::create();
  6. 6 a->setCreated(true);
  7. 7 return a.release();
  8. 8}
  9. 9
  10. 10RefPtr<Node> b = createSpecialNode();
NodeオブジェクトはNode::createの中のadoptRefを呼ぶことによってPassRefPtrにセットされaに渡され、aでreleaseされてbに渡される。この間、参照カウントを変更することは全くない。

なお、RefCountedクラスは生成されてから最初にadoptRefが呼ばれることなしにrefやderefが呼ばれた場合、アサーションが失敗するようにランタイムでチェックしている。

ガイドライン

私たちは、WebKitのコードにおけるRefPtrやPassRefPtrの使い方のガイドラインを作成した。

ローカル変数
  • 所有権と生存期間が保証されるなら、ローカル変数は生ポインタで良い。
  • 所有権か生存期間の保証を保持する必要があるなら、RefPtrを使うべき。
  • PassRefPtrを使うべきではない。

データメンバ
  • 所有権と生存期間が保証されるなら、ローカル変数は生ポインタで良い。
  • 所有権か生存期間の保証を保持する必要があるなら、RefPtrを使うべき。
  • PassRefPtrを使うべきではない。

関数の引数
  • 関数がオブジェクトの所有権を取らなければ、引数は生ポインタにすべき
  • 関数が所有権を取るなら、引数はPassRefPtrにすべき。殆どのセッター関数がそうなる。引数の使い方が至極単純でないなら、引数は関数の最初でRefPtrへ変換すべき。

関数の返り値
  • 返り値がオブジェクトだが、所有権が移動しないならば、返り値は生ポインタにするべき。殆どのゲッター関数がそうなる。
  • 返り値が新しいオブジェクトか、何らかの理由で所有権が移動するならば、その返り値はPassRefPtrでなければならない。ローカル変数は概してRefPtrだから、RefPtrからPassRefPtrにするためreturn文でreleaseを呼ぶのが普通だ。

新しいオブジェクト
  • 新しいオブジェクトは生成後、スマートポインタで全ての参照カウントを自動的に行うために、できる限り早くRefPtrへ入れなければならない。
  • RefCountedオブジェクトにおいて、上記はadoptRef関数を用いるべき。
  • 一番良い書き方は、プライベートなコンストラクタを使い、PassRefPtrを返すパブリックなcreate関数を使うことだ。

Improving this document

未訳