ぼやきごと/2012-03-18/C++型消去:派生クラス型を引数に取る関数オブジェクトを基本クラスに登録して呼び出す のバックアップソース(No.1)

#blog2navi()
*C++型消去:派生クラス型を引数に取る関数オブジェクトを基本クラスに登録して呼び出す [#l6f0d84b]

次のようなコードを考えます。

#code(c){{
#include <functional>
#include <string>
#include <memory>
#include <iostream>

namespace
{
    ///--------------------
    /// ベースクラス
    class Character
    {
    protected:
        /// コンストラクタ。
        Character() { ]

    public:
        /// デストラクタ。
        virtual ~Character() { }

        /// 派生クラス型 TSelf のインスタンスを引数に取る
        /// 関数オブジェクト TFuncObj を登録する。
        /// 呼び出し時に自身のインスタンス(this)を渡す。
        template<class TSelf, class TFuncObj>
        void setFunc(TFuncObj func)
        {
            /// @todo 実装してください
        }

        /// 事前に登録した関数オブジェクトを呼び出す。
        /// 引数には自身のインスタンス(this)を渡す。
        void doFunc()
        {
            /// @todo 実装してください
        }

    private:
        // コピー禁止
        Character(const Character&);
        void operator=(const Character&);
    };

    ///--------------------
    /// 派生クラス1
    class Suika : public Character { };

    ///--------------------
    /// 派生クラス2
    class Meiling : public Character { };

    ///--------------------
    /// 派生クラス3
    class Tenshi : public Character { };

    ///--------------------
    /// 操作クラス
    class Controller
    {
    public:
        /// 特定の派生クラスで初期化する。
        template<class TChara>
        void initialize()
        {
            std::auto_ptr<TChara> chara(new TChara());

            // 関数オブジェクトを登録する
            chara->setFunc<TChara>(
                std::bind1st(
                    std::mem_fun1(&Controller::procCallback<TChara>),
                    this));

            // 基本クラスにアップキャストして保持
            _chara = chara;
        }

        /// 初期化した内容で処理を実行する。
        void execute()
        {
            // initialize で登録した関数オブジェクトを呼び出す
            _chara->doFunc();
        }

    private:
        /// 関数オブジェクト用の基本定義。
        template<class TChara>
        void procCallback(TChara*)
        {
            std::cout << "(Basic function)" << std::endl;
        }

        /// Suika 用の特殊化定義。
        template<>
        void procCallback(Suika*)
        {
            std::cout << "Ibuki Suika" << std::endl;
        }

        /// Meiling 用の特殊化定義。
        template<>
        void procCallback(Meiling*)
        {
            std::cout << "Chugo... Hong-Meiling!!" << std::endl;
        }

    private:
        std::auto_ptr<Character> _chara;
    };
}

///--------------------
/// メイン関数。
int main(int, char**)
{
    Controller ctrl;

    // Suika で初期化して実行
    ctrl.initialize<Suika>();
    ctrl.execute();

    // Meiling で初期化して実行
    ctrl.initialize<Meiling>();
    ctrl.execute();

    // Tenshi で初期化して実行
    ctrl.initialize<Tenshi>();
    ctrl.execute();

    return 0;
}
}}

長ったらしくてわかりにくいですが、要するに…

+ 66行目で派生クラス型を引数に取る関数オブジェクトを事前に登録し、
+ 79行目で事前に登録した関数オブジェクトを呼び出す。

…という処理を基本クラス側の実装で実現するにはどうすればいいのかということです。

実行結果としては次のような出力を期待しています。

#pre{{
Ibuki Suika
Chugo... Hong-Meiling!!
(Basic function)
}}

解法としては、 @code{boost::function}; 等にも使われている''型消去(Type Erasure)''というテクニックが使えます。~
関数オブジェクト型がコピー可能であると仮定した時の @code{Character}; クラスの実装例は次の通りです。

#code(c){{
// …前略…
    class Character
    {
    private:
        /// 関数オブジェクトの実行と破棄を定義する。
        template<class TSelf, class TFuncObj>
        struct Invoker
        {
            /// 関数オブジェクトを実行する。
            static void call(void* funcObj, void* self)
            {
                TFuncObj* func = static_cast<TFuncObj*>(funcObj);
                (*func)(static_cast<TSelf*>(self));
            }

            /// 関数オブジェクトを破棄する。
            static void dispose(void* funcObj)
            {
                delete static_cast<TFuncObj*>(funcObj);
            }
        };

        void* _funcObj;                     ///< 関数オブジェクト
        void (*_invokeCall)(void*, void*);  ///< 関数オブジェクト実行関数
        void (*_invokeDispose)(void*);      ///< 関数オブジェクト破棄関数

    protected:
        /// コンストラクタ。
        Character() : _funcObj(0), _invokeCall(0), _invokeDispose(0)
        {
        }

    public:
        /// デストラクタ。
        virtual ~Character()
        {
            resetFunc();
        }

        /// 派生クラス型 TSelf のインスタンスを引数に取る
        /// 関数オブジェクト TFuncObj を登録する。
        /// 呼び出し時に自身のインスタンス(this)を渡す。
        template<class TSelf, class TFuncObj>
        void setFunc(TFuncObj func)
        {
            // 以前に登録されていたら破棄
            resetFunc();

            // 関数オブジェクトをコピーして void* で持つ
            _funcObj = new TFuncObj(func);

            // 関数オブジェクト操作関数を持つ
            typedef Invoker<TSelf, TFuncObj> Invoker;
            _invokeCall = &Invoker::call;
            _invokeDispose = &Invoker::dispose;
        }

        /// 事前に登録した関数オブジェクトを呼び出す。
        /// 引数には自身のインスタンス(this)を渡す。
        void doFunc()
        {
            if (_funcObj != 0)
            {
                _invokeCall(_funcObj, this);
            }
        }

        /// 登録されている関数オブジェクトを破棄する。
        void resetFunc()
        {
            if (_funcObj != 0)
            {
                _invokeDispose(_funcObj);
                _funcObj = 0;
            }
        }

    private:
        // コピー禁止
        Character(const Character&);
        void operator=(const Character&);
    };
// …後略…
}}

まず注目すべきは49〜55行目です。

50行目では関数オブジェクトをコピーし、 @code{void*}; として保持しています。~
この時点で @code{_funcObj}; からは引数 @code{func}; の持っていた型情報が消去されています。~
これが型消去と呼ばれる理由です。

ではこの手法は型安全ではないのかというとそうではなく、型情報は54,55行目で保持している関数ポインタが持つことになります。~
これらの関数は @code{Character::Invoker<TSelf, TFuncObj>}; テンプレートクラスの静的メンバ関数であり、''関数そのものは非テンプレート関数と同じでありながらテンプレート型情報を持つことができます''。~
つまりこれらの関数によって型情報を復元することが可能であるというわけです。

それを踏まえて10〜14行目を見ると、関数に渡された引数を関数が持つ型情報でキャストして型復元していることがわかります。~
この関数の呼び出しは64行目で行われており、第一引数には50行目で保持した関数オブジェクトが渡されているため、型が一致することは保証されます。~
そのため、 @code{void*}; 型からのキャストでありながら型安全が保証されているというカラクリが成立するわけです。

なお、この実装では @code{setFunc}; メンバ関数のテンプレート型引数 @code{TSelf}; に適当な型…例えば @code{int}; を渡すと、 @code{this}; を強引に @code{int*}; 型にキャストして呼び出してしまいます。~
これを防ぐためには @code{TSelf}; 型が @code{Character}; クラスの派生クラスであるかコンパイル時にチェックする処理を入れるとよいでしょう。~
例えばboostが使えるならば @code{setFunc}; メンバ関数の頭に次のコードを入れればいいはずです(もちろん必要なヘッダはインクルードしている前提で)。

#code(c,nomenu,nonumber,nooutline,noliteral,nocomment){{
BOOST_STATIC_ASSERT(boost::is_base_of<Character, TSelf>::value);
}}

RIGHT:Category: &#x5b;[[C++>ぼやきごと/カテゴリ/C++]]&#x5d;&#x5b;[[プログラミング>ぼやきごと/カテゴリ/プログラミング]]&#x5d; - 2012-03-18 02:21:52
----
RIGHT:&blog2trackback();
#comment(above)
#blog2navi()