カウンタ付き参照

※本エントリ内のファイルの公開を終了しました。

C++ では、new を用いてオブジェクト (メモリ領域) を確保したならば、これを delete でを明示的に解放してやらなければなりません。 この「借りたもの (メモリ) は自分で返す」という硬派なスタイルは非常に C++ らしく、またパフォーマンスの面でも優れるため、個人的にはとても気に入っています。

その一方で、このスタイルにはメモリリークや二重解放によるアクセス違反, ダングリング・ポインタの発生とった問題があり、致命的なバグの原因となりがちであることも事実です。 また、プログラムの性質上、オブジェクトが利用されなくなるタイミングが予測しづらく、明示的な解放を記述することが原理的に困難であるような状況というものも存在します。

C/C++ 以外の殆どの言語はガーベジ・コレクションを言語仕様として採用することでこうした問題を回避しています。 特に、ガーベジ・コレクション手法の一つである参照カウントは、その動作原理が非常に単純であり、またそれ故に (一般的な状況下における) パフォーマンスにおいて他の手法よりも優れています。 そこで、この参照カウントの仕組みを C++ でも利用できないかと考え、Reference というクラステンプレートを作ってみました:

ソースコードは上記リンク先のページからダウンロードすることができます。

今回はこの Reference クラステンプレートの使い方について簡単に説明します。

動作テスト用クラスの作成

参照カウンタの動作確認のため、オブジェクトの確保・解放状況を標準出力へモニタする Test クラスを定義します。

Test.h
#ifndef __TEST_H__
#define __TEST_H__

class Test {
public:

    //construction
    Test();

    //destruction
    virtual ~Test();

    //operation
    void PrintAddress() const;

};

#endif //__TEST_H__
//[EOF]
Test.cpp
#include "Test.h"
#include <stdio.h>

//コンストラクタ
Test::Test(){
    printf("Object #%p constructed.\n", this);
}

//デストラクタ
Test::~Test(){
    printf("Object #%p destructed.\n", this);
}

//アドレスの表示
void Test::PrintAddress() const {
    printf("this => #%p.\n", this);
    return;
}

//[EOF]

解放処理の自動化

ポインタの代わりに Reference オブジェクトを使用すると、解放を自動化することができます。 実際に「いつ」オブジェクトが解放されるのかを確認してみましょう。

#include <stdio.h>

#include "Reference.h"
#include "Test.h"

int main(){

    Reference<Test> r =Reference<Test>(new Test);

    puts("Check point A passed.");

    r =Reference<Test>::null;

    puts("Check point B passed.");

    return 0;
}

//[EOF]
Object #003B30D0 constructed.
Check point A passed.
Object #003B30D0 destructed.
Check point B passed.

new で生成され、Reference オブジェクトに割り当てられたオブジェクト (これを「ターゲット」は、これを指す Reference オブジェクトの数 (参照カウント) が 0になった時点で自動的delete によって解放されます。 (* そのため、スタック上のオブジェクトや、new[] で生成されたオブジェクトをターゲットとして指定すると動作が不定となるので注意してください。)

上記のプログラムでは、r に null が代入された時点で自動解放が実行されています。

参照カウントの変化

先に述べたとおり、ターゲットの解放は、参照カウントが 0となった時点で実行されます。 参照カウントは、同一のターゲットを共有する参照 (Reference オブジェクト) の数なので、これが複製されるたびに 1ずつ増加します。 参照カウントの値は、Reference の (正確には、その基本クラスである ConstReference の) メンバ関数 Count で取得することができます。

#include <stdio.h>
#include "Reference.h"
#include "Test.h"

int main(){

    Reference<Test> r1 =Reference<Test>(new Test);
    Reference<Test> r2;

    printf("r1.Count() => %d.\n", r1.Count());
    printf("r2.Count() => %d.\n", r2.Count());

    puts("Check point A passed.");
    r2 =r1;

    printf("r1.Count() => %d.\n", r1.Count());
    printf("r2.Count() => %d.\n", r2.Count());

    puts("Check point B passed.");
    r1 =Reference<Test>::null;

    printf("r1.Count() => %d.\n", r1.Count());
    printf("r2.Count() => %d.\n", r2.Count());

    puts("Check point C passed.");
    r2 =Reference<Test>::null;

    printf("r1.Count() => %d.\n", r1.Count());
    printf("r2.Count() => %d.\n", r2.Count());

    return 0;
}

//[EOF]
Object #003B30D0 constructed.
r1.Count() => 1.
r2.Count() => 0.
Check point A passed.
r1.Count() => 2.
r2.Count() => 2.
Check point B passed.
r1.Count() => 0.
r2.Count() => 1.
Check point C passed.
Object #003B30D0 destructed.
r1.Count() => 0.
r2.Count() => 0.

ターゲットを持たない参照 (NULL参照) から Count を呼び出すと 0が返されます。

ターゲットの参照

Reference オブジェクトは、ポインタと同様、*, -> 演算子によってターゲットおよびそのメンバを参照することができます。 また、ターゲットが指定されている (NULLでない) かどうかの判定も、ポインタと同様に行うことができます。 これは、bool へのキャスト演算子 (operator bool) が定義されているためです。

//例 1
void foo(Reference<Test> r){

    if (r)
        r->PrintAddress();
    else
        puts("Null reference specified.");

    return;
}

//例 2
bool bar(Reference<int> r){

    if (!r) return false;

    *r =rand();

    return true;
}

その他の機能

この他にも、Reference クラスはポインタに類似した機能を持っています。 それについては次回のエントリでまとめたいと思います。