C++編【言語解説】 第31章 RTTI

この章の概要

この章の概要です。

RTTI

この章のテーマは、RTTI (RunTime Type Infomation、実行時型情報)です。 RTTI は、プログラムの実行中に参照できるように管理された型に関する情報のことで、 また、これを利用することを指します。

RTTI を使えば、例えば「あるオブジェクトは何型か?」を実行中に知ることができます。 継承を活用しているとき、この機構が役に立つことがあります。 例えば、Baseクラスから公開継承によって派生した Derivedクラスがあるとして、次のコードを見て下さい。

void func(Base* b)
{
	// b は本当に Base のオブジェクトか?
}

func関数に渡された Base のポインタは、本当に Base のオブジェクトを指しているでしょうか? それとも、本当は Derived のオブジェクトを指しているのでしょうか? プログラマがコードをしっかり理解あるいは管理していない限り、普通、この区別は付きません。

もちろん、区別を付けないからこそ、多態性を活用することができ、継承の価値が生まれるのですが、 どうしても、区別を付けたいこと、付けなければならない場面も稀にあります。 そこで、RTTI を利用することになります。

typeid

型情報を取得するには、typeid演算子を使用します。 typeid演算子を使用するためには、typeinfo という標準ヘッダのインクルードが必要です。
なお、typeid演算子をオーバーロードすることはできません(第19章参照)。

#include <typeinfo>
const std::type_info& info = typeid(int);

typeid演算子のオペランドとして、何らかの型名を与えると、その型の型情報への参照が返されます。 ここで返される参照は、const std::type_info&型です。 std::type_info は、標準ライブラリに含まれているクラスです。
また、const参照なので、参照先を書き換えることはできません。

std::type_infoクラスのオブジェクトは、コピーすることができないようになっています。 そのため、typeid演算子が返す結果は、素直に const参照で受け取るか、&演算子を適用して、ポインタで受け取る必要があります。 (C++11以降においては、ムーブもできません)

typeid演算子に式を与えた場合、その式の結果の型についての型情報が返されます。 例えば、ポインタ変数があるとき、これを間接参照した結果を得られます。

int* p;
typeid(*p);

この場合、p を間接参照した先の型、つまり int型の型情報が返されます。
重要なのはここからで、多態性が発揮される状況下であれば、実際の型に応じて、動的な結果を返します。 例えば、

class Base {
public:
	virtual void f(){}
};
class Derived : public Base {};

このようなクラスがあるとき、

Base* p;
typeid(*p);

このコードが、どのような結果になるかは、実行時に p が何を保持しているかに依ります。 p が本当に Baseオブジェクトを指しているなら、Base型の型情報を返しますし、 Derivedオブジェクトを指しているなら、Derived型の型情報を返します。
また、もう1つの可能性として、p がヌルポインタの場合がありますが、 このときには、std::bad_typeid例外が送出されます。 (例外については第32章を、 std::bad_typeid については【標準ライブラリ】第17章を参照)。

この動作は多態性を必要としていますから、このように結果が変化するのは、 Baseクラスが仮想関数を持っている場合に限られます。 もし、仮想関数が無い場合は、たとえ p がヌルポインタであったとしても、常に Base型の型情報を返します


type_infoクラスのメンバの中で重要なのは、nameメンバ関数と、==演算子および !=演算子でしょう。

nameメンバ関数は、型の名前を表現する文字列を const char*型で返します。 この文字列が具体的にどのように表現されているかは、コンパイラの実装依存です。 そのため、あるコンパイラで確認した結果が、他のコンパイラでの結果と一致する保証は無いので、 表現に依存するプログラムを書いてはいけません。

#include <iostream>
#include <typeinfo>

class Base {
public:
	virtual void f(){}
};

class Derived : public Base {};

int main()
{
	Base b;
	Derived d;
	Base* p1 = &b;
	Base* p2 = &d;

	std::cout << typeid(b).name() << "\n"
	          << typeid(d).name() << "\n"
	          << typeid(p1).name() << "\n"
	          << typeid(*p1).name() << "\n"
	          << typeid(p2).name() << "\n"
	          << typeid(*p2).name() << std::endl;
}

実行結果(VisualC++ 2013):

class Base
class Derived
class Base *
class Base
class Base *
class Derived

実行結果(clang 3.7):

4Base
7Derived
P4Base
4Base
P4Base
7Derived

前述した通り、型を表現する方法は環境依存になります。 VisualC++ での結果は割とストレートです。 clang の方はやや暗号めいた感じがしますが、 よく見ると、先頭に「P」が付くとポインタなのだろうなどと推測はできそうです。

実行結果の中で、typeid(*p2).name() の結果が重要です。 p2自身は、Baseクラスのポインタですが、実際には Derivedオブジェクトを指していますから、 *p2 を与えると、Derived であるという結果が得られます。


type_infoクラスの ==演算子を使うと、2つの type_infoオブジェクトが、同じ型に関する型情報かどうかを知ることができます。 勿論、!=演算子は、一致しないことを確認します。

#include <iostream>
#include <typeinfo>

class Base {
public:
	virtual void f(){}
};

class Derived : public Base {};

int main()
{
	Base b;
	Derived d;
	Base* p1 = &b;
	Base* p2 = &d;

	std::cout << std::boolalpha
	          << (typeid(p1) == typeid(p2)) << "\n"
	          << (typeid(*p1) == typeid(*p2)) << std::endl;
}

実行結果:

true
false


type_infoクラスに関する詳細は、【標準ライブラリ】第15章でも扱っています。 そちらの章では、他にも、標準ライブラリに含まれているユーティリティ機能を紹介しています。

アップキャストとダウンキャスト

これまでの章で見てきた通り、公開継承関係にあるクラス間では、基底クラス型を要求する箇所に、派生クラスのオブジェクトを渡すことができます。 これは、自動的に型変換がなされている訳ですが、継承構造の下から上へ向かう方向なので、アップキャストと呼ばれます。 このようにアップキャストは暗黙的かつ安全に行われることが保証されています。

一方、継承構造の上から下へ向かう型変換をダウンキャストと呼びます。 ダウンキャストは、安全性を放棄すれば、次のように static_cast(第7章)を使って行えます。

class Base {};
class Derived : public Base {};

Base* b = new Derived();
Derived* d = static_cast<Derived*>(b);

ダウンキャストを reinterpret_cast(第7章)を使って行うコードを見かけることがありますが、その方法は、正しく動作する保証がありません。 なぜなら、ポインタを他のクラスを指すように変換すると、アドレスの調整が必要になる可能性があるためです。 ビットレベルで何も変更を加えないのが reinterpret_cast なので、調整が行われることはありません。

この例のように、Base型のポインタが実際には Derivedクラスのオブジェクトを指しているということが分かっていれば、 static_cast を使ってダウンキャストを行っても問題ありません。 しかし、あるクラスの派生クラスは複数存在する可能性がありますから、常に安全であるとは限りません。

class Base {};
class Derived1 : public Base {};
class Derived2 : public Base {};

Base* b = new Derived1();
Derived2* d = static_cast<Derived2*>(b);

この場合、Derived2 が Base の派生クラスであることは分かっているので、ダウンキャストであることは確実ですが、 実際には b が指しているのは Derived1 のオブジェクトです。 そのため、b を Derived2 へ型変換するのは安全では無く、実際、この後 d を使って、メンバへアクセスしようとすると、 未定義の動作になります。

このように、static_cast を使うと、安全でないダウンキャストを受け付けてしまいます。 キャストがコンパイルエラーにならないからといって、正しく動作するとは限らないということです。

dynamic_cast

前の項での説明のように、static_cast によるダウンキャストは、安全性の面で問題があるかも知れません。 そこで、安全であるかどうかを動的に判断したうえでキャストを行う dynamic_cast を使用します。 C++ で追加された他の3つのキャスト(static_castreinterpret_castconst_cast。いずれも第7章を参照) と同様の構文になっていますが、動的に機能するという点が大きく異なっています。

typeid演算子と同様、 dynamic_cast を使ったダウンキャストを機能させるには、仮想関数が含まれている必要があります。 また、必然的に、dynamic_cast の対象は、継承構造を持ったオブジェクトを指すポインタか参照でなければなりません

詳しくは触れませんが、多重継承において、兄弟となるクラス間でのクロスキャストを行う際にも、dynamic_cast を使用できます。 これも機能させるには、仮想関数の存在が必要です。 また、dynamic_cast を明示的なアップキャストのために使うことができますが、 基底クラスの存在は派生クラス側からでも常に分かるので、これには仮想関数は不要です(ただ、そもそもアップキャストは暗黙的に行えます)。

もし、変換することができない型へ dynamic_cast を行おうとした場合、 指定したものがポインタであれば、変換先のポインタ型で表現されるヌルポインタが返され、 参照であれば std::bad_cast例外(【標準ライブラリ】第17章)が送出されます

ポインタと参照とで、キャスト失敗に対する処置が異なることに注意して下さい。 参照の場合、ヌルポインタ相当な表現が存在しないため、例外機構が使われます。

実際の使用例を挙げます。 参照の場合については、第32章で取り上げる例外処理を使用しているので、 そこは無視しておいても結構です。

#include <iostream>

class Base { virtual void f(){} };
class Derived1 : public Base {};
class Derived2 : public Base {};

int main()
{
	Base* const b = new Derived1();

	Derived2* const pd = dynamic_cast<Derived2*>(b);
	if (pd != NULL) {
		std::cout << "OK" << std::endl;
	}
	else {
		std::cout << "ERROR" << std::endl;
	}

	try {
		Derived2& rd = dynamic_cast<Derived2&>(*b);
		std::cout << "OK" << std::endl;
	}
	catch (std::bad_cast&) {
		std::cout << "ERROR" << std::endl;
	}

	delete b;
}

実行結果:

ERROR
ERROR

Baseクラスに仮想関数が必要であることに注意して下さい。
ポインタ変数 b は、Derived1クラスのオブジェクトへのポインタを保持しているので、Derived2*型への変換は失敗します。 ポインタ版ならヌルポインタになっていないかどうかを、参照版なら例外が送出されていないかどうかをチェックすれば、 不正なメンバアクセスなどを起こさないように、処理を続行させることができます。


ダウンキャストには危険性がつきまとっているので、 そもそもダウンキャストが必要にならないようにコードを書くことが望ましいと言えます。

ダウンキャストが必要になる理由は、派生クラスが固有で持っているメンバを使いたいからのはずです。
そこで、最初から、基底クラスの型で扱わず、派生クラスの型で扱うようにすれば、ダウンキャストを避けられます。 あるいは、基底クラスに仮想関数を用意して、派生クラスでオーバーライドすれば、基底クラスのメンバだけで操作できます。 しかし、いずれの方法も、設計の観点から望ましくないこともあるでしょうから、その場合に限ってダウンキャストを行って下さい

ダウンキャストが安全に行えるかどうか確証を持てる場面では、static_cast を使って構いません。 その場合、コンパイル時点でキャストの処理は終わっているので、実行時に余分なコストが発生せず、効率的でもあります。
安全性に確証が持てない場合は、dynamic_cast を使い、キャストの失敗に備えたコードを書いて下さい。 dynamic_cast は、キャストそのもの、失敗の判定、失敗時の処理を含めて、いずれも動的な処理なので、 実行時にコストが発生します。


練習問題

問題@ 仮想関数を含んでいない場合に、typeid が返す結果が動的な型に基づいた結果を返せないことを確認して下さい。


解答ページはこちら

参考リンク

更新履歴

'2016/10/15 clang の対応バージョンを 3.7 に更新。

'2016/5/29 新規作成。



前の章へ

次の章へ

C++編のトップページへ

Programming Place Plus のトップページへ