C++

shared_ptrとweak_ptrの使い分けわかる?

More than 1 year has passed since last update.

はじめに

本当に怖いC++erとC++という糞言語 で「スマートポインタ(弱参照)に対する知識が無いC++erは嫌だ」と言われてしまったので、勉強することにした。

リソース管理

プログラムにおけるリソースとは、メモリやファイルなど、OSによって管理されているものを指す。C++では確保したリソースを解放する責任はプログラマが負っているため、解放忘れなどのミスによってリソースリークが起こり、デバッグに数時間/数日間苦しめられる場合もある。つまりC++を使う場合は、 確保したリソースは必ず一度だけ解放しなくてはならない という法則を肝に銘じてプログラミングを行う必要がある。

スマートポインタ

「確保したリソースをプログラマが忘れずに必ず一度だけ解放する必要がある」と言ったものの、これをプログラマが 常にミスすること無く 行うことは非常に難しく、できることならJavaやC#の様にリソース管理を言語機能やフレームワークに任せてしまいたい。実はC++にもリソース管理を自動化してくれるスマートポインタという機能があり、C++11では標準ライブラリでスマートポインタを使用することができる。もしC++11を業務で使うことが許されないアドバンストでない会社にお勤めの方は、Boostライブラリからスマートポインタを使用することができる。

所有権

スマートポインタを利用することでリソースを安全に扱えるからと言って、プログラマがリソース管理について無知/無関心でいて良い訳ではない。C++でリソースを正しく管理するためには 所有 という概念について理解する必要がある。所有とは、リソースの所有者が下記の権利と義務を負うという考えである。

  • 自身が所有しているリソースは所有者以外の者からは決して解放されないという権利
  • 自身が所有しているリソースを最後に解放するという義務

プログラマにかわり、上記権利を守り上記義務を負ってくれるのがスマートポインタである。

shared_ptrとweak_ptr

C++11/Boostライブラリで提供されているスマートポインタには、 unique_ptr, shared_ptr, scoped_ptr, intrusive_ptr, weak_ptr など、複数種類存在するが、今回はこの中からタイトルに挙げた shared_ptrweak_ptr の使いどころを説明をする。

その前にまずは shared_ptrweak_ptr はそれぞれどんな特徴を持つのか?について、簡単に説明をする。

shared_ptr

shared_ptr とは、 指定されたリソースへの所有権を共有するスマートポインタ である。参照カウント方式のリソース管理を行う。参照カウント方式のリソース管理とは、オブジェクトを指しているポインタの数をカウントしておき、カウントが0になると自動で delete を呼び出すリソース管理手法である。

shared_ptr.cpp
#include <iostream>
#include <string>
#include <memory>
#include <cassert>

using namespace std;

class Hoge
{
    private:
        string s_;
    public:
        explicit Hoge(const string &s)
            :s_(s)
        {
            cout << "Hoge::Hoge(" + s_ + ")" << endl;
        }

        ~Hoge()
        {
            cout << "Hoge::~Hoge(" + s_ + ")" << endl;
        }
};

int main()
{
    shared_ptr<Hoge> hoge_ptr0;

    {
        cout << "---AとBの構築---" << endl;
        shared_ptr<Hoge> hoge_ptr1(new Hoge("A"));
        shared_ptr<Hoge> hoge_ptr2(new Hoge("B"));
        shared_ptr<Hoge> hoge_ptr3(new Hoge("C"));

        // この時点でA,B,Cのカウント数は1である
        assert(hoge_ptr1.use_count() == 1);
        assert(hoge_ptr2.use_count() == 1);
        assert(hoge_ptr3.use_count() == 1);

        cout << "---Aの破棄---" << endl;
        hoge_ptr0 = hoge_ptr1 = hoge_ptr2; // ここでAは誰からも参照されなくなるので、Aは破棄される

        // hoge_ptr0とAがBを指す様になったのでBのカウント数は3となる
        assert(hoge_ptr2.use_count() == 3);
        assert(hoge_ptr3.use_count() == 1);

        cout << "---Cの破棄---" << endl;
    } // このスコープを抜けるとCを参照する者がいなくなるので、Cは破棄される

    cout << "---Bの破棄---" << endl;
    return 0;
} // ここでBを参照する者がいなくなるので、Bは破棄される
---AとBの構築---
Hoge::Hoge(A)
Hoge::Hoge(B)
Hoge::Hoge(C)
---Aの破棄---
Hoge::~Hoge(A)
---Cの破棄---
Hoge::~Hoge(C)
---Bの破棄---
Hoge::~Hoge(B)

weak_ptr

weak_ptr とは shared_ptrのオブザーバー である。下記コードを見てもらえば分かる通り、 weak_ptrshared_ptr が共有するリソースの所有権について何ら影響を与えない。言い換えると weak_ptrはshared_ptrが共有するリソースの所有権を持たないため、weak_ptrの監視中に監視対象のリソースを破棄されてしまう 可能性があるのだ。この様な性質を 弱い参照 と呼ぶ。

weak_ptr.cpp
#include <iostream>
#include <string>
#include <memory>
#include <cassert>

using namespace std;

class Hoge 
{
    private:
        string s_; 
    public:
        explicit Hoge(const string &s) 
            :s_(s)
        {   
            cout << "Hoge::Hoge(" + s_ + ")" << endl;
        }   

        ~Hoge()
        {   
            cout << "Hoge::~Hoge(" + s_ + ")" << endl;
        }   
};

int main()
{
    weak_ptr<Hoge> weak;

    {   
        // この時点でAのカウント数は1である
        shared_ptr<Hoge> shared0(new Hoge("A"));
        assert(shared0.use_count() == 1); 

        // shared0の参照者が増えると、参照カウント数も増える
        shared_ptr<Hoge> shared1= shared0;
        assert(shared0.use_count() == 2); 

        // weakがshared0を監視するも、
        // 参照カウント数は増えない
        weak = shared0;
        assert(shared0.use_count() == 2); 
    } // ここでshared_ptrは無効になる

    // 監視しているshared_ptrの寿命は無効
    assert(weak.expired());
    return 0;
}

shared_ptrとweak_ptrの使いどころ

shared_ptrweak_ptr の特徴と使い方を見てきたが、これら2つはどの様に使い分ければ良いのだろうか?

使い分けの判断は 所有権を持つこと無く共有リソースの監視/利用をしたいかどうか だと思う。所有権を持つこと無く共有リソースの監視/利用をしたい場合は weak_ptr を使うことになるのだろう。例えば下記の様な循環する参照を shared_ptr で扱うと、Hoge/Fugaのデストラクタが呼ばれずリソースリークが発生する。

circ_ref1.cpp
#include <iostream>
#include <string>
#include <memory>
#include <cassert>

using namespace std;

struct Fuga;

struct Hoge 
{
    shared_ptr<Fuga> p;

    ~Hoge() { cout << "Hoge::~Hoge()" << endl; }
};

struct Fuga 
{
    shared_ptr<Hoge> p;

    ~Fuga() { cout << "Fuga::~Fuga()" << endl; }
};

int main()
{
    shared_ptr<Hoge> hoge_ptr(new Hoge());
    shared_ptr<Fuga> fuga_ptr(new Fuga());

    hoge_ptr->p = fuga_ptr;
    fuga_ptr->p = hoge_ptr;

    return 0;
}

循環参照の問題を解決するには、一方は shared_ptr を保持し、もう片方はその shared_ptr への weak_ptr を保持させれば良い。例えば下記の様なコードになるだろう。

circ_ref2.cpp
#include <iostream>
#include <string>
#include <memory>
#include <cassert>

using namespace std;

struct Fuga;

struct Hoge
{
    shared_ptr<Fuga> p;

    ~Hoge() { cout << "Hoge::~Hoge()" << endl; }
};

struct Fuga
{
    weak_ptr<Hoge> p;

    ~Fuga() { cout << "Fuga::~Fuga()" << endl; }
};

int main()
{
    shared_ptr<Hoge> hoge_ptr(new Hoge());
    shared_ptr<Fuga> fuga_ptr(new Fuga());

    hoge_ptr->p = fuga_ptr;
    fuga_ptr->p = weak_ptr<Hoge>(hoge_ptr);

    return 0;
}

参考資料