第50章 型チェック

 今回は、アップキャストをした場合元の型を知るにはどうすればいいかということについて話したいと思います。


 では、今回の要点です。


 では、いってみましょう。


 忍者がいます。忍者にもいろいろな人がいます。いろいろな階級の人がいます。ある階級の忍者しか入れないような場所もあるでしょう。

 忍者を CNinja クラスにして、上忍クラス CJohnin 、中忍クラス CChuhnin 、下忍クラス CGenin を派生するとします。そして、上忍しか入れない謎の場所を CSecret クラスとします。

 では、CSecret に入れるかどうかの判定はどのクラスでするのがベストでしょうか? もし CSecret の様な場所がいくつもあるとすれば、CSecret の中で判定するとよさそうだと分かります。場所を増やすたびに CJohnin 等のクラスを変えるというのも、何かおかしな気がします。(注:絶対この仕様がいいとは言えないかもしれません。)

 ここで問題があります。判定関数はどういう風に作ればいいのでしょうか?

 引数は CJohnin, CChuhnin, CGenin の全てのオブジェクトを渡せる必要があります。つまり、CNinja への const 参照にするといいでしょう。そして、あとはこの参照からどの階級の人かを判定すればいいわけですが、どうすればいいんでしょうね?

 つまり、参照からどの階級の人かを取得できればいいわけです。仮想関数を使って何か値を返すようにし、その値で判定するという方法がとれるでしょう。

 しかし、折角もともとの型が違うのですから、そのことを利用してどうにかできないものでしょうか。

 そういうときのために、C++では型の情報を取得する演算子が用意されています。それが typeid です。

プログラム
// Typeid1.cpp
#include <iostream.h>
#include <typeinfo.h>

// int を bool に変換
inline bool IntToBool(int a){ return a != 0; }

// 忍者クラス
class CNinja                  { public: virtual ~ CNinja(); };
class CJohnin : public CNinja { public: virtual ~CJohnin(); };
class CChunin : public CNinja { public: virtual ~CChunin(); };
class CGenin  : public CNinja { public: virtual ~ CGenin(); };

 CNinja::~ CNinja() { }
CJohnin::~CJohnin() { }
CChunin::~CChunin() { }
 CGenin::~ CGenin() { }

// 場所クラス(抽象クラス)
// これから派生しておけば、場所を系統的に扱えます
class CPlace
{
public:
    virtual ~CPlace();
    virtual bool CanEnter(const CNinja& ninja) = 0;
};

// 謎の場所クラス
class CSecret : public CPlace
{
public:
    bool CanEnter(const CNinja& ninja);
};

CPlace::~CPlace()
{
}

// 入れるかどうかの判定
bool CSecret::CanEnter(const CNinja& ninja)
{
    return IntToBool(typeid(ninja) == typeid(CJohnin));
}

int main()
{
    CSecret secret;
    CJohnin johnin;
    CChunin chunin;
    CGenin  genin;

    const char* str[] = {
        "入れません。",
        "入れます。",
    };

    cout << "上忍は" << str[secret.CanEnter(johnin)] << endl;
    cout << "中忍は" << str[secret.CanEnter(chunin)] << endl;
    cout << "下忍は" << str[secret.CanEnter( genin)] << endl;

    return 0;
}
実行結果
上忍は入れます。
中忍は入れません。
下忍は入れません。

 これをVC++でコンパイルするには、dynamic_cast のときと同じようにランタイムタイプ情報(RTTI)を有効にする必要があります(第2部第42章参照)。また、dynamic_cast の制限と同じく、正しく動かすには仮想関数が必ず1つは必要です。

 見てもらえれば分かるように、CanEnter 関数で入れるかどうかの判定を行っています。その判定文は

return IntToBool(typeid(ninja) == typeid(CJohnin));

です。typeid(ninja) で ninja の本来の型の情報を取得することができます。これを CJohnin の型情報を比べて等しければ true で(入れて)、等しくなければ flase になります(入れません)。感覚的に分かりやすくていいですね。

 IntToBool というのは、単に int を bool に変換する関数です。型情報の比較の結果は int で返ってくるので、bool に直してやっているだけです。bool へのキャストは警告が出るのでいちいちこうやっています。


 上のプログラムにはまだよく分からないところがあると思います。そうです。型情報、型情報と抽象的に言っていますが、型情報とは具体的にどういうものなのかということです。

 型情報は実は type_info クラスへの const 参照として得られます。そのため、type_info クラスの定義のある typeinfo ヘッダファイルが必要になります。上の比較も、type_info::operator== で行っています。この戻り値は何故か int になっていて、それで bool で返すのに IntToBool のようなことをしているわけです。

 では、型情報を type_info クラスへの const 参照で受けて、何かやってみましょう。

プログラム
// Typeid2.cpp
#include <iostream.h>
#include <typeinfo.h>

// 忍者クラス
class CNinja                 { public: virtual ~CNinja(); };
class CGenin : public CNinja { public: virtual ~CGenin(); };

CNinja::~CNinja() { }
CGenin::~CGenin() { }
CGenin::~CGenin() { }

void DispType(const CNinja* pNinja)
{
    cout << "pNinja の型名は "
         << typeid(pNinja).name() << " です。" << endl;
    cout << "*pNinja の型名は "
         << typeid(*pNinja).name() << " です。" << endl;
}

int main()
{
    CGenin genin;

    cout << "int の型名は "
         << typeid(int).name() << " です。"<< endl;
    cout << "type_info の内部名は "
         << typeid(type_info).raw_name() << " です。" << endl;
    cout << "main の型の内部名は "
         << typeid(main).raw_name() << " です。" << endl;

    DispType(&genin);

    return 0;
}
実行結果例
int の型名は int です。
type_info の内部名は .?AVtype_info@@ です。
main の型の内部名は .P6AHXZ です。
pNinja の型名は class CNinja const * です。
*pNinja の型名は class CGenin です。

 type_info は == と != の比較の他に、型名を文字列として取得することができます。そのため関数が name と raw_name です。

 name はそのまま型名を表示しますが、raw_name は第47章で話したのと似たような型の内部表現を返します。raw は「生(なま)の」という意味なので、生の名前を返すというわけです。関数名の内部表現と同じく型の内部表現も一意であることが保証されているので、2つの型名が同じかどうかはこの raw_name を比較するのが安全です。

 type_info は参照として受け取り、しかも代入演算子、コピーコンストラクタは public にありません。型情報を保存しておいて後で比較するという場合には、この内部表現を保存しておけばいいわけです。

 まだ注意することがありますね。DispType には CGenin へのアドレスを渡しているわけですが、pNinja の型は const CNinja* になっています。つまり、ポインタの状態ではポインタの型をそのまま取得することになります。*pNinja というように、参照して初めて本当の型を知ることができるのです。


 補足しますが、ポリモーフックな(仮想関数を持った)クラスへの参照の型判定には、メンバに埋め込まれた情報が使われます。つまり、おかしなものを入れると例外が投げられます。

 例えば、上の DispType に NULL を渡すとどうなるか考えてみましょう。pNinja の型は上に書いたとおり const CNinja* になるでしょう。しかし、*pNinja というのは NULL を使って参照しているわけで、メンバに埋め込まれた情報とやらを使うことはできません。つまり、型を特定することができません。この場合は bad_typeid 型の例外が投げられます。エラーメッセージは bad_alloc と同じく what 関数で取得できます。

 bad_alloc も bad_typeid も exception というクラスから派生しています。つまり、catch(exception& e) を使えば両方とも取得することができます。what は exception クラスの仮想関数なので、エラーメッセージも表示することができます。

 dynamic_cast も参照へ代入する際には bad_cast 例外が投げられる場合がありますが、これもやはり exception から派生しています。C++が言語仕様として投げる例外は全て exception クラスから派生しています。


 最後にもう1つ補足ですが、gcc では type_info::operator== の戻り値は bool です。IntToBool は必要ありません。そして、raw_name は存在せず、name がそのまま内部表現を返します。他の開発環境でも何か違いがあるかも知れませんね。


 では、今回の要点です。


 51章からはいよいよ多重継承について話していきたいと思います。それでは。


P.S.) もう機種依存の話は嫌です(汗)。gcc は頻繁にヴァージョンアップするので最新の規格に対応してくれていますが、VC++6.0 は結構前に出たのでいろいろまずいところがあるんです。例えば、VC++ はメンバ関数テンプレートのサポートがまだされていません。次のヴァージョン VisualStudio.Net が正式公開されたらちゃんと対応してくれていることを祈っておきましょう。(2001年5月6日 記ス)


第49章 破壊と創造 | 第51章 多重継承事始

Last update was done on 2001.5.6

この講座の著作権はロベールが保有しています