Home / ぼやきごと / 2012-03-18 / C++型消去:派生クラス型を引数に取る関数オブジェクトを基本クラスに登録して呼び出す
C++型消去:派生クラス型を引数に取る関数オブジェクトを基本クラスに登録して呼び出す

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

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

すべて開くすべて閉じる
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
 
 
 
 
 
 
-
-
|
!
-
|
-
-
|
|
-
!
|
-
|
|
!
|
-
-
!
|
-
|
!
-
-
!
|
|
-
!
|
!
 
-
|
!
 
-
|
!
 
-
|
!
 
-
|
!
-
|
-
!
|
-
|
|
-
!
|
|
|
|
-
!
!
|
-
!
-
-
!
!
|
|
-
!
|
-
|
!
|
-
!
|
-
|
!
|
-
!
|
-
|
!
|
|
|
!
!
|
-
 
!
-
 
 
-
!
 
 
-
!
 
 
-
!
 
 
 
!
#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;
}

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

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

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

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

Ibuki Suika
Chugo... Hong-Meiling!!
(Basic function)

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

すべて開くすべて閉じる
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
-
!
-
|
-
!
|
-
-
!
-
|
|
!
|
-
!
-
|
!
!
|
|
|
|
|
|
-
!
-
!
|
|
-
!
-
|
!
|
-
|
|
!
|
-
-
!
|
-
!
|
-
!
|
|
!
|
-
|
!
-
|
-
|
!
!
|
-
!
-
|
-
|
|
!
!
|
|
-
!
|
!
-
// …前略…
    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行目では関数オブジェクトをコピーし、 void* として保持しています。
この時点で _funcObj からは引数 func の持っていた型情報が消去されています。
これが型消去と呼ばれる理由です。

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

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

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

BOOST_STATIC_ASSERT(boost::is_base_of<Character, TSelf>::value);
Next: [C++型消去:派生クラス型を〜 の別種]
Category: [C++][プログラミング] - 2012-03-18 02:21:52