Flat Leon Works

アプリやゲームを作ってます。

【C++】弱参照クラスを自作する

C++11から標準ライブラリにstd::weak_ptrが入りました。std::weak_ptrを使うことで弱参照を行えるようになるのですが、ちょっと使い勝手が悪いので弱参照クラスを自作してみました。

ちなみに、弱参照の良さについてはこちら↓で紹介しています。

【C++】弱参照のすすめ

はじめに

弱参照という言葉の定義

弱参照は「ガベージコレクションにてオブジェクトの延命を行わない参照」のことですが、一般的な仕様として「オブジェクトの削除を検知できる機能」も持っています。この記事ではそこに注目し、オブジェクトの削除を検知できる参照という意味で弱参照という言葉を使用しています。

なので、この記事で作成する弱参照クラスはガベージコレクションとは関係ありません。他の名称を付けるなら、ダングリングポインタにならない安全なポインタという意味で「SafePointer」とかでしょうか。または、自動で無効化されるということで「AutoNullPointer」とか…?まぁ、とりあえずこの記事中では弱参照として「WeakPtr」という名前にします。

std::weak_ptrを使わない理由

弱参照といえば、C++11から標準ライブラリにstd::weak_ptrが入りましたが、以下のような仕様になっており少し使い勝手が悪いです。

  • std::shared_ptrからしstd::weak_ptrを作ることができないので、利用できる場面が限られる
  • std::weak_ptrから直接参照先にアクセスできない(std::shared_ptrを経由する必要がある)

std::weak_ptrstd::shared_ptrからしか作ることができないので、自動変数やメンバ変数から作ることができません。これはだいぶ不便です。

紹介するコードの注意点

  • C++11以降が必須です
  • 紹介するコードは自由に使ってもらって構いませんが、あまりちゃんと動作テストをしていないので参考程度にすることをおすすめします

自作してみた弱参照クラスのソースコード

// WeakPtr.h

#include <assert.h>
#include <type_traits>

class PtrInfo
{
public:
    void* m_Ptr      = nullptr;
    int   m_RefCount = 0;

    PtrInfo( void* ptr ) : m_Ptr( ptr ){}
    void Inc( void ){ ++m_RefCount; }
    static void Dec( PtrInfo*& p_info )
    {
        if (!p_info){ return; }
        --p_info->m_RefCount;
        if ( p_info->m_RefCount == 0 )
        {
            delete p_info;
            p_info = nullptr;
        }
    }
    bool IsNull( void ) const { return m_Ptr == nullptr; }
    template<class T> T* GetPtr( void ) const { return reinterpret_cast<T*>(m_Ptr); }
    int GetRefCount( void ) const { return m_RefCount; }
};

template<class T>
class WeakPtrController;

#define nonvirtual

template<class T>
class WeakPtr
{
    friend class WeakPtrController<T>;
    PtrInfo* m_pPtrInfo = nullptr;
public:
    nonvirtual ~WeakPtr()
    {
        PtrInfo::Dec( m_pPtrInfo );
    }
    WeakPtr()
    {
    }
    explicit WeakPtr( PtrInfo* p_ptrInfo ) : m_pPtrInfo( p_ptrInfo )
    {
        if ( m_pPtrInfo )
        {
            m_pPtrInfo->Inc();
        }
    }
    WeakPtr( const WeakPtr& other ) : m_pPtrInfo( other.m_pPtrInfo )
    {
        if ( m_pPtrInfo )
        {
            m_pPtrInfo->Inc();
        }
    }
    WeakPtr& operator=( const WeakPtr& other )
    {
        PtrInfo::Dec( m_pPtrInfo );
        m_pPtrInfo = other.m_pPtrInfo;
        if ( m_pPtrInfo )
        {
            m_pPtrInfo->Inc();
        }
        return *this;
    }
    void Clear( void )
    {
        PtrInfo::Dec( m_pPtrInfo );
        m_pPtrInfo = nullptr;
    }

    template<class U>
    WeakPtr<U> GetUpcasted( void )
    {
        static_assert( std::is_base_of<U,T>::value, "" ); // U が T の基底クラスである必要がある
        return WeakPtr<U>( m_pPtrInfo );
    }

    template<class U>
    WeakPtr<U> GetDowncasted( void )
    {
        static_assert( std::is_base_of<T,U>::value, "" ); // U は T の派生クラスである必要がある
        if ( dynamic_cast<U*>( GetPtr() ) ) // ポインタが実際に U型 インスタンスであるかどうかをチェック
        {
            return WeakPtr<U>( m_pPtrInfo );
        }
        return WeakPtr<U>( nullptr ); // ダウンキャストに失敗したらNULLポインタ(のWeakPtr)を返す
    }

    bool IsNull( void ) const { return !m_pPtrInfo || m_pPtrInfo->IsNull(); }
    T* GetPtr( void ) const { return m_pPtrInfo ? m_pPtrInfo->GetPtr<T>() : nullptr; }
    T& operator*() const { assert( !IsNull() ); return *GetPtr(); }
    T* operator->() const { assert( !IsNull() ); return GetPtr(); }
    operator T*() const { return GetPtr(); }

    int GetRefCount( void ) const { return m_pPtrInfo ? m_pPtrInfo->GetRefCount() : 0; }
};

template<class T>
class WeakPtrController
{
    WeakPtr<T> m_WeakPtr;
public:
    nonvirtual ~WeakPtrController()
    {
        if ( m_WeakPtr.m_pPtrInfo )
        {
            m_WeakPtr.m_pPtrInfo->m_Ptr = nullptr;
        }
    }
    explicit WeakPtrController( T* ptr ) : m_WeakPtr( new PtrInfo( ptr ) ){}

    WeakPtrController() = delete;
    WeakPtrController( const WeakPtrController& other ) = default;
    WeakPtrController& operator=( const WeakPtrController& other ) = default;

    template<class U>
    WeakPtr<U> GetDowncasted_unsafe( U* p_this )
    {
        (void)p_this;
        static_assert( std::is_base_of<T,U>::value, "" );
        return WeakPtr<U>( m_WeakPtr.m_pPtrInfo );
    }

    WeakPtr<T> GetWeakPtr( void ){ return m_WeakPtr; }
};

#define DEF_WEAK_CONTROLLER(type_) \
    public:  WeakPtr<type_> GetWeakPtr( void ){ return m_WeakPtrController.GetWeakPtr(); } \
    protected: WeakPtrController<type_> m_WeakPtrController{this}

#define DEF_WEAK_GET(type_) public: WeakPtr<type_> GetWeakPtr( void ){ return m_WeakPtrController.GetDowncasted_unsafe( this ); }class{}

特徴

  • クラス定義時に仕込みを行う必要あり
  • インスタンスの作成時に特別なことをする必要なし
  • 自動変数でも利用可能
  • アップキャストやダウンキャストにも対応
  • shared_ptrやunique_ptrと組み合わせて使うことも可能

使い方

#include "WeakPtr.h"

class A
{
    DEF_WEAK_CONTROLLER( A ); // WeakPtrを作れるようにするための仕込み
public:
    virtual ~A(){}
    void Print( const char* p_mes ) const
    {
        printf( "%s\n", p_mes );
    }
};

class B : public A
{
    /* 基底クラスAですでに仕込みが行われているので不要
    DEF_WEAK_CONTROLLER( B );
    */
    DEF_WEAK_GET( B ); // 必須ではないが、これを仕込んでおくと GetWeakPtr() が派生クラス型になる
public:
};

class X
{};

int main()
{
    // 基本的な使い方
    {
        WeakPtr<A> weakA;
        {
            A a;
            weakA = a.GetWeakPtr();
            if ( A* pA = weakA.GetPtr() ) // このようにif文でポインタを受け取るとオブジェクトの生存チェックが同時にできて楽
            {
                pA->Print( "[1]" );
            }
            if ( A* pA = weakA ) // T*への暗黙の型変換あり
            {
                pA->Print( "[2]" );
            }
        }
        if ( A* pA = weakA.GetPtr() ) // aは破棄されたのでNULLポインタが返る
        {
            pA->Print( "[2] Not print" );
        }
    }

    // アップキャスト
    {
        B* pB = new B;
        WeakPtr<B> weakB = pB->GetWeakPtr();

        /* 暗黙の変換によるアップキャスト
        WeakPtr<A> weakA = pB->GetWeakPtr(); // 変換演算子を用意していないのでコンパイルエラー
        */

        // 専用関数(GetUpcasted)でのアップキャスト
        WeakPtr<A> weakA  = pB->GetWeakPtr().GetUpcasted<A>();

        // 基底クラスの方のGetWeakPtr()を使うことでアップキャストせずに基底クラスのWeakPtrを取得することも可能
        WeakPtr<A> weakA2 = pB->A::GetWeakPtr();

        /* 無関係なクラスへのアップキャストはコンパイル時にエラー
        WeakPtr<X> weakX = pB->GetWeakPtr().GetUpcasted<X>();
        */
    }

    // ダウンキャスト
    {
        B* pB = new B;
        WeakPtr<A> weakA = pB->GetWeakPtr().GetUpcasted<A>();

        /* 暗黙の変換によるダウンキャスト
        WeakPtr<B> weakB2 = weakA; // 変換演算子を用意していないのでコンパイルエラー
        */

        // 専用関数(GetDowncasted)でのダウンキャスト
        WeakPtr<B> weakB2 = weakA.GetDowncasted<B>(); // 実行時にダウンキャストが行われる。weakAの参照先がBのインスタンスでない場合はダウンキャストに失敗しweakB2はNULLになる。
        assert( weakB2.GetPtr() != nullptr ); // 今回はweakAの参照先がBのインスタンスなので、NULLにはならない。

        // ダウンキャスト失敗例
        {
            A* pA = new A;
            WeakPtr<A> weakA2 = pA->GetWeakPtr();
            WeakPtr<B> weakB3 = weakA2.GetDowncasted<B>(); // BはAの派生クラスなのでコンパイルは通るが、実行時にダウンキャストに失敗し、weakB3はNULLになる。
            assert( weakB3.GetPtr() == nullptr );
        }

        /* 無関係なクラスへのダウンキャストはコンパイル時にエラー
        WeakPtr<X> weakX  = weakA.GetDowncasted<X>();
        */
    }

    // shared_ptrやunique_ptrと組み合わせて使うことも可能
    {
        std::shared_ptr<B> sharedB = std::make_shared<B>();
        WeakPtr<B> weakB = sharedB->GetWeakPtr();
        assert( weakB.GetPtr() != nullptr );
        sharedB.reset();
        assert( weakB.GetPtr() == nullptr );

        {
            std::unique_ptr<B> uniqueB(new B);
            weakB = uniqueB->GetWeakPtr();
            assert( weakB.GetPtr() != nullptr );
        }
        assert( weakB.GetPtr() == nullptr );
    }

    return 0;
}

基本的な使い方は以下のとおりです。

  • 弱参照を得られるようにしたいクラス定義にDEF_WEAK_CONTROLLERを仕込む
  • そのクラスのインスタンスからGetWeakPtr()で弱参照(WeakPtr<T>)を取得
  • WeakPtr<T>::GetPtr()でポインタを取得( 参照先のオブジェクトが削除されていたらNULLが返る )

その他細かいところ

  • DEF_WEAK_CONTROLLERを仕込んだクラスの派生クラスにはDEF_WEAK_CONTROLLERは不要
  • DEF_WEAK_CONTROLLERを仕込んだクラスの派生クラスにDEF_WEAK_GETを仕込むことで、GetWeakPtr()が派生クラス型になる
  • GetUpcasted<T>を使うことでT型へアップキャストされたWeakPtr型を取得できる(型が相応しくない場合はコンパイルエラーになる)
  • GetDowncasted<T>を使うことでT型へダウンキャストされたWeakPtr型を取得できる(型が相応しくない場合はコンパイルエラーになる。型が相応しくても実行時の型が相応しくない場合はNULLになる)

解説

クラス一覧

  • WeakPtr : 弱参照クラスです
  • WeakPtrController : 弱参照を生成するためのクラスです。弱参照を扱えるようにしたいクラスにメンバ変数として持たせます
  • PtrInfo : ポインタ情報です。こWeakPtrControllerとWeakPtr間で共有されます

弱参照が自動で無効化される仕組み

各弱参照は参照先オブジェクトへのポインタを直接持つのではなく「参照先オブジェクトへのポインタを持つクラス」へのポインタを持つようにします。実際のクラス名で説明すると、PtrInfoが実際の参照先オブジェクトへのポインタを持ち、WeakPtrはそのPtrInfoへのポインタを持つという形になります。このPtrInfoは同じ参照先を表すWeakPtr間で共有されるので、PtrInfoが持つ参照先オブジェクトへのポインタがNULLになると、そのPtrInfoを共有しているすべてのWeakPtrでオブジェクトへの参照がNULLになるという仕組みです。

このPtrInfoに参照先オブジェクトのポインタをセットしたり、NULLにしたりするのがWeakPtrControllerの仕事です。また、WeakPtrを生成するのもWeakPtrControllerの仕事です。

WeakPtrControllerは初期化時にコンストラクタで引数として受け取ったポインタを「PtrInfoが持つ参照先オブジェクトへのポインタ」にセットします。そしてデストラクタでNULLをセットします。なので、WeakPtrControllerをメンバ変数として持たせた上で初期化時にthisポインタを渡すことで、「WeakPtrControllerをメンバ変数として持ったクラス」が破棄されるときに自動で「PtrInfoが持つ参照先オブジェクトへのポインタ」にNULLがセットされます。これで、オブジェクトが死ぬとそのオブジェクトを参照していた弱参照が無効化されるという仕組みの完成です。

PtrInfoWeakPtrWeakPtrControllerの関係を図にするとこんな感じです。

PtrInfoを共有する仕組み

先ほど、WeakPtrPtrInfoを共有すると説明しました。さて、あるオブジェクトを共有するには「オブジェクトの所有権」、つまり、オブジェクトの生存期間の管理ができる必要があります。例えば、オブジェクトを自動変数として生成してしまうとオブジェクトの生存期間をそのスコープに限れられてしまうので、オブジェクトの所有権を持っているとは言えなくなります。

オブジェクトの所有権を持つには、基本的にはnewでオブジェクトを生成する必要があります。WeakPtrで共有するPtrInfoも、所有権を管理するためにnewで生成されます。

問題はnewしたPtrInfoをいつdeleteするかです。PtrInfoは共有されるものなので、最後の所有者がPtrInfoを放棄するときにdeleteすべきです。これはまさにstd::shared_ptrのような挙動です。そしてPtrInfoは、実際にstd::shared_ptrのように参照カウンタで自動でdeleteされるようになっています。

WeakPtrWeakPtrControllerは生成/破棄のたびに、共有するPtrInfoの参照カウントを加算/減算します。そして減算時に参照カウントが0になったらPtrInfoをdeleteします。

つまり、弱参照の内部の実装に強参照が使われているというわけです。ややこしいですけど。

使い勝手を良くする工夫

上記、「弱参照が自動で無効化される仕組み」と「PtrInfoを共有する仕組み」で弱参照クラスとして最低限の機能が実現できますが、使い勝手を良くするために以下のようなものも用意しました。

  • WeakPtrControllerの用意を楽にするDEF_WEAK_CONTROLLERマクロ
  • 派生クラスでは派生型のWeakPtrを取得できるようにするためのDEF_WEAK_GETマクロ
  • WeakPtrをポインタのように扱えるようにするための演算子オーバーロード(->,*,変換演算子)
  • WeakPtrをアップキャスト/ダウンキャストするための関数

アップキャスト/ダウンキャストでは、static_assertstd::is_base_ofを使うことで、そもそも継承関係にない型への変換はコンパイル時にエラーになるようにしました。C++11様様です。

改善案

今回作った弱参照クラス、いくつか改善できる箇所があります。

  • PtrInfoをプールで使い回すようにする
  • ムーブセマンティクス対応
  • マルチスレッド対応
PtrInfoをプールで使い回すようにする

現在の実装ではPtrInfoをnewでヒープメモリから確保するようにしています。これは、PtrInfoを共有できるようにヒープなどの独立したメモリ上に配置する必要があるためですが、PtrInfoのサイズは固定なのでPtrInfoのプールを用意することで高速化とメモリの断片化の回避になります。

プールを使用するとPtrInfoの個数に上限ができてしまいますが、PtrInfoは64bit環境でも1つ16バイトなのでプールサイズをかなり大きめにしても問題ないでしょう。

ムーブセマンティクス対応

WeakPtrのコピーは、PtrInfoへのポインタのコピーと参照カウントの増減くらいなので、たいした処理コストではないと思いますが、それでもムーブに対応すればPtrInfoへのポインタのコピーだけで済むようになるので結構おいしいかもしれません。

マルチスレッド対応

今回の実装ではマルチスレッドが全く考慮されていません。同じ参照を指す複数のWeakPtrが別々スレッドに存在する場合まともに動かないはずです。マルチスレッド対応するのか、マルチスレッド対応バージョンのWeakPtrを別に用意するのか、それともマルチスレッド非対応を明確にし別スレッドを検知したらabortするようにするのか…、何かしら対策は必要かもしれません。

参考リンク