2014年6月21日土曜日

C++11 スマートポインタの使い方・用途・サンプル

C++はガーベジコレクションがないので、動的にallocateしたオブジェクトは明示的にdeallocateしなければなりません。これを怠るとメモリリークなどもの問題が出てくるわけです。また逆に、オブジェクトを破棄したポインタを使いつづけるとnullポインタ、或いはダングリングポインタとしてエラーの原因になります。

C++において生来的ともいえるこれらの問題を解決しようというのがスマートポインタの機能です。簡単に言うと、ポインタのスコープに合わせてオブジェクトが勝手にdeallocate(deconstruct)されます。これによってオブジェクトとポインタの寿命が一致するのでメモリリーク、ダングリングポインタが防げます。

スマートポインタには何種類かあり、それぞれ異なる仕様・用途があります。以下、unique_ptr, shared_ptr, weak_ptr, auto_ptr(C++11以前)の説明をします。


1. unique_ptr

一番使われるのはunique_ptrでしょう。以上に述べたスマートポインタの特徴に加え、名の通り「指しているオブジェクトに対する唯一のポインタ」であることを保証します(低レベルで色々やれば崩せますが)。即ち、unique_ptrは他のスマートポインタからオブジェクトをコピーしたりアサインしたりすることが出来ません。

オブジェクトを他のポインタに割り当てたい場合はstd::moveを使う必要があります。これを使うと元のポインタはnullポインタになるので、やはり一つのポインタしかそれを指すことが出来ません。

以下のコードのように、いちいちdeleteなどしなくても使うことができます。


void unique_ptr_test() {
    cout << "----- unique_ptr test -----" << endl;
    std::unique_ptr<int> first(new int(1));
    std::unique_ptr<int> second(new int(2));

    // 関連して、nullptrも新しく加わった。0, NULLと区別される。
    std::unique_ptr<int> nullpo(nullptr);

    cout << "*first = " << *first << endl;
    cout << "*second = " << *second << endl;

//    std::unique_ptr<int> second = first; ERROR: コピーは出来ない。
//    second = first; ERROR: アサインも出来ない。

    // moveを用いることでオブジェクトをsecondに移すことが出来る。
    // そのとき、firstはnullptrになるので注意。
    // つまり、unique_ptrを参照できるポインタは常に一つである。(定義より)
    second = std::move(first);
    cout << endl << "second = std::move(first);" << endl << endl;
    cout << "*fisrt = " << (first? "not null" : "null") << endl;
    cout << "*second = " << *second << endl << endl;

    cout << "nullpo is " << (nullpo? "not null" : "null") << endl;

    // unique_ptrはスコープの外に出るときにdeconstructorを呼ぶ。
    {
        std::unique_ptr<Foo> locale(new Foo());
    }
}


2. shared_ptr

unique_ptrの使いやすさの一つに「オブジェクトを指す唯一のポインタ」であることがあるでしょうが、逆にそうでない方が使いやすい場合もあるでしょう。複数のスマートポインタで同じオブジェクトを指したい時に使われるのがshared_ptrです。

shared_ptrはunique_ptrと異なり、コピー・アサインをすることができます。また、オブジェクトを指している複数のshared_ptrのうちの一つだけを初期化したい場合にreset()を呼ぶことで、そのオブジェクトを破棄せずに(それによって他のポインタがnullポインタにならずに)そのポインタだけをnullポインタにすることができます。

また、共有されているオブジェクトはshared_ptrの全てがスコープから外れた時に破棄されます。



void shared_ptr_test() {
    cout << "----- shared_ptr test -----" << endl;
    std::shared_ptr<Foo> first(new Foo());
    std::shared_ptr<Foo> copy = first;

    // copyがまだあるのでresetでもオブジェクトは持続される。
    // しかしfirstはnullポインタになる。
    first.reset();
    cout << "first = " << (first? "not null" : "null") << endl;
    cout << "copy = " << (first? "not null" : "null") << endl;

    // copyもreset()が呼ばれるとdestructor
    copy.reset();
}


3. weak_ptr

 weak_ptrはshared_ptrをサポートするスマートポインタです。
weak_ptrはshared_ptrが指しているオブジェクトを同様に指すことが出来ます。しかし、先ほどshared_ptrの指すオブジェクトは、「shared_ptrの全てがスコープから外れたら破棄される」とありましたが、つまりweak_ptrが指していても、shared_ptrが全てなくなれば破棄される、ということです。

ではweak_ptrは何に使われるのかというと、 shared_ptrを用いてオブジェクトを破棄して新しいオブジェクトをconstructしたり、という流れの中で使われます。つまり、weak_ptrでshared_ptrのオブジェクトを参照しておき、それからshared_ptrで新しいオブジェクトをallocateすると、weak_ptrはNullを指すことになります。使ってみるとこれが便利な場合が結構あります。



void weak_ptr_test() {
    cout << "----- weak_ptr test -----" << endl;


    // 通常のポインタはDangling pointerとなるリスクがある。
    // 例えばここでrefはdangling pointerである。
    int* ptr = new int(10);
    int* ref = ptr;
    delete ptr;

    // それに対して、
    // weak_ptrは対象のオブジェクトがdeallocateされたかをexpired(), lock()で確認することが出来る。

    std::shared_ptr<int> sptr;
    // takes ownership of pointer
    sptr.reset(new int(10));

    // weak1は*sptrを参照はするが、保持はしない。
    std::weak_ptr<int> weak1 = sptr;

    // 前のオブジェクトを破棄し、新しいオブジェクトを作る。
    // ここでweak1がshared_ptrなら一緒に5を指すことになるが、ここではweak_ptrなので参照オブジェクトがなくなる。
    sptr.reset(new int(5));

    // 5を指すweak_ptr
    std::weak_ptr<int> weak2 = sptr;


    // weak1の指していたオブジェクトがdeallocateされたかはlock()で確認することが出来る。
    if ( auto tmp = weak1.lock() ) {
        std::cout << "weak1.lock() = " << *tmp << '\n';
    } else {
        std::cout << "weak1 is expired\n";
    }

    if ( auto tmp = weak2.lock() ) {
        std::cout << "weak2.lock() = " << *tmp << '\n';
    } else {
        std::cout << "weak2 is expired\n";
    }

}


4. auto_ptr (C++11以前)

 auto_ptrはレガシーです。簡単に言うならunique_ptrの下位互換です。C++11以前からあるので(これを発展させたものが今まで説明してきたスマートポインタら)、C++11を使いたくない事情がある場合に代用として使えます。基本的に使い方はunique_ptrと同じです。ただ隠蔽構造がやや不完全で、例えば以下のコードだとy = xでunique_ptrのy = std::move(x)に当たり、ここでxはnullポインタになるので、なかなか注意が必要な構造をしています。



void auto_ptr_test() {
    cout << "----- auto_ptr test -----" << endl;
    auto_ptr<int> x(new int(5));
    auto_ptr<int> y;

    y = x;

    cout << "x is " << (x.get() ? "not null" : "null") << endl; // Print NULL
    cout << "y is " << (y.get() ? "not null" : "null") << endl; // Print non-NULL address i
}


スマートポインタは通常のポインタと比べてそんなに遅くないそうです(アセンブリにして追加一行程度とのこと)。特にパフォーマンスが求められていない部分のコードは手軽く使っていけるようです。

0 件のコメント:

コメントを投稿