RTTI | Programming Place Plus C++編【言語解説】 第31章

トップページC++編

C++編で扱っている C++ は 2003年に登場した C++03 という、とても古いバージョンのものです。C++ はその後、C++11 -> C++14 -> C++17 -> C++20 と更新されており、今後も 3年ごとに更新されます。
なかでも C++11 での更新は非常に大きなものであり、これから C++ の学習を始めるのなら、C++11 よりも古いバージョンを対象にするべきではありません。特に事情がないなら、新しい C++ を学んでください。 当サイトでは、C++14 をベースにした新C++編を作成中です。

この章の概要

この章の概要です。


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;
}

実行結果(Visual Studio 2017 (x86)):

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

実行結果(clang 5.0.0):

4Base
7Derived
P4Base
4Base
P4Base
7Derived

前述したとおり、型を表現する方法は環境依存になります。Visual Studio での結果はわりとストレートです。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 を行おうとした場合、指定したものがポインタであれば、変換先のポインタ型で表現されるヌルポインタが返され、参照であれば 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 が返す結果が動的な型に基づいた結果を返せないことを確認してください。


解答ページはこちら

参考リンク


更新履歴

’2018/4/5 VisualStudio 2013 の対応終了。

’2018/4/2 「VisualC++」という表現を「VisualStudio」に統一。

’2018/1/5 Xcode 8.3.3 を clang 5.0.0 に置き換え。

’2017/7/30 clang 3.7 (Xcode 7.3) を、Xcode 8.3.3 に置き換え。

’2017/5/18 「アップキャストとダウンキャスト」の項を、2つの項に分離した。

≪さらに古い更新履歴を展開する≫



前の章へ (第30章 多重継承)

次の章へ (第32章 例外)

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

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る