C++のキャスト
なぜキャストが必要か?
コンパイラは、あるメモリの最初のアドレスを知ったところで、それがどこまで続くのか、或いはどのメンバがどの位置に現れるかということを事前に知っていないといけません。そうしないと計算できませんからね。だから、ある変数xが型Aなのか型Bなのか与えることで、そのサイズが4byteなのか8byteなのか、はては1byteのメンバを4つ持っているのかを判別することができるのです。
さて、C++にはよく使う2つのキャストがあります。static_castとdynamic_castですね。あとは特殊なconst_castとなんでも変換できてしまうreinterpret_castがあります。今回はstatic_castとdynamic_castに焦点を絞ってまとめてみたいと思います。
ポインタでないキャスト
値同士のキャスト
intやdouble間のキャストについては、暗黙の変換が施されます。たとえば次のように書いてもコンパイルは通りますし、期待されるようにy = 3.0となります。
int x = 3; double y = x; // 3.0
ただし、符号がついた場合は期待されない動きになるかもしれません。基本的にデータ自体を変えないのだから、期待された動作といえば期待された動作ではありますが…
int x = -3; unsigned int y = x; // 4294967293
異なるクラス間のキャスト
基本的に型の違うもの同士のキャストは許されません。
struct A { int x; }; struct B { char d[4]; }; int main() { A a { 10 }; B b { { 1, 2, 3, 4 } }; a = b; // エラー return 0; }
継承関係にあるクラス間のキャスト
継承関係にある場合、派生クラスBから派生元クラスAへのキャスト〔アップキャストといいます〕は可能ですが、その逆、派生元クラスAから派生クラスBへのキャスト〔ダウンキャストといいます〕はできません。
struct A { int x; }; struct B : public A { int y; }; int main() { A a; B b; a = b; b = a; // エラー return 0; }
ポインタ同士のキャスト
本題です。ポインタ同士でキャストする必要が出てくる場合があります。大きく分けて2通りの状況があります。1つはポリモーフィズムを利用する場合。もう1つは動的な型を実現する場合です。
継承関係にあるキャスト
先ほどのようにアップキャストの場合は暗黙に行われ、かつ問題ありません。暗黙に行われるものは自然なのでstatic_castを書かないのが私のルールです。
struct Base { int x; }; struct Derived : public Base { int y; }; int main() { Derived d; Base* b = &d; // static_cast<Base*>(d)でもよい return 0; }
ところが、ダウンキャストは暗黙には行われません。そもそもダウンキャストは危険を伴います。たとえば次のように、ダウンキャストを行うと…
struct Base { int x; }; struct Derived : public Base { int y; }; int main() { Base b; Derived* d = &b; // Error Derived* d = static_cast<Derived*>(&b); // OK return 0; }
たとえ、static_castを使ってコンパイルが通ったとしても、もとのBase*型のポインタが本当にDerived*を指しているのかは不明です。今回は指していませんので、当然d->yとするとアクセス違反となります。なぜなら、実体はBase型で、強制的にDerived型にしてアクセスしているからyなんて存在しないからです。
ポリモーフィズム
派生元クラスにvirtual修飾子の付いたメンバ関数が存在すると、そのクラスはポリモーフィックななクラスであるといいます。ポリモーフィックなクラスを継承すると、そのクラスから派生元にアップキャストしたときに、派生したクラスの情報が付加されます。これが仮想関数テーブルです。
struct Base { virtual void func() = 0; virtual ~Base() = default; }; struct DerivedA : public Base { void func() { cout << "A::func()" << endl; } }; struct DerivedB : public Base { void func() { cout << "B::func()" << endl; } }; int main() { DerivedA da; DerivedB db; Base* ba = &da; Base* bb = &db; ba->func(); // A::func() bb->func(); // B::func() return 0; }
このように、同じ型Base*のfuncを呼び出したのにもかかわらず、実行結果がキャストする前の型DerivedAとDerivedBのものに依存していますね。これがC++の言語におけるポリモーフィズムです。実行時型情報とは関係ありません。
ポリモーフィズムは、Baseがインターフェイスクラスの場合〔=virtualなメンバ関数だけで構成されるクラス〕あるいは、抽象クラス〔virtualなメンバ関数を持つクラス〕で行われます。ここで言えるのは、派生したクラスは平等に扱われるべきということです。これは動的ではあるのですが、限定的な動的であるといえます。
動的な型
どういうことかというと、たとえばユーザーから入力されるクラスA, Bがあったとします。ところが、受け付けたいクラスはAだけで、Bは受け付けたくないとします。雰囲気は次の通りです。nはユーザーからの入力を模擬しているだけで、実際には、そのif-else文が入力そのものとなります。そしてbの部分では、いろいろな入力のうち、なんとか型をチェックして弾きたいのです。インターフェースでは、そもそもどれも平等でなければならないので出来ません。とはいっても、入力自体は平等に受け付けたいので、そこだけはインターフェースというか抽象クラスとして存在しています。
struct Base { virtual ~Base() = default; }; struct DerivedA : public Base { }; struct DerivedB : public Base { }; int main() { DerivedA da; DerivedB db; int n = 1; Base* b; if(n % 2 == 0) { b = &da; } else { b = &db; } b; // ここではDerivedAだけ欲しい… return 0; }
ここで実行時型情報の出番です。dynamic_castでは、実行時型情報を利用して、元のオブジェクトの型に適切に変換できれば変換したポインタを返し、そうでなければnullptrを返します。ただし、参照型にキャストする場合は例外が投げられるので注意です。
int main() { DerivedA da; DerivedB db; int n = 0; Base* b; if(n % 2 == 0) { b = &da; } else { b = &db; } DerivedA* pa = dynamic_cast<DerivedA*>(b); if(pa == nullptr) { cout << "失敗" << endl; } else { cout << "成功" << endl; } return 0; }
この実行時型情報は、ユーザーからの入力に動的に対応できるので、たとえばスクリプト言語――Luaとか――にユーザーデータをCから作って、それを受け取るときなどに大変有効です。ダウンキャストの時でもとの型があっているか分からない〔=動的である〕ときに有効な方法ですね。