syghの新フラグメント置き場

プログラミングTipsやコード断片の保管場所です。お絵描きもときどき載せます。

ダウンキャストに潜む罠

(これは2012-05-18に書いた故OCNブログの記事を移植したものです)

C++のダウンキャスト(特に多重継承した場合)にまつわるお話です。

例えばこんな感じ。

#include <cstdio>
#include <conio.h>

class Base1
{
protected:
    virtual ~Base1()
    {
        puts(__FUNCSIG__);
    }
};

class Base2
{
protected:
    virtual ~Base2()
    {
        puts(__FUNCSIG__);
    }
};

class Derived
    : public Base1
    , public Base2
{
};

int main()
{
    {
        Derived d;

        Base1* pb1 = &d;
        Base2* pb2 = &d;

        printf("&d  = 0x%p\n", &d);
        printf("pb1 = 0x%p\n", pb1);
        printf("pb2 = 0x%p\n", pb2);
        puts("** Down cast:");
        printf("static_cast<Derived*>(pb1)      = 0x%p\n", static_cast<Derived*>(pb1)); // OK。
        printf("reinterpret_cast<Derived*>(pb1) = 0x%p\n", reinterpret_cast<Derived*>(pb1)); // 一応 OK だが static_cast や dynamic_cast のほうが好ましい。
        printf("dynamic_cast<Derived*>(pb1)     = 0x%p\n", dynamic_cast<Derived*>(pb1)); // OK。
        puts("** Down cast:");
        printf("static_cast<Derived*>(pb2)      = 0x%p\n", static_cast<Derived*>(pb2)); // OK。
        printf("reinterpret_cast<Derived*>(pb2) = 0x%p\n", reinterpret_cast<Derived*>(pb2)); // コンパイルは通るが不正。
        printf("dynamic_cast<Derived*>(pb2)     = 0x%p\n", dynamic_cast<Derived*>(pb2)); // OK。
        puts("** Cross cast:");
        printf("reinterpret_cast<Base1*>(pb2) = 0x%p\n", reinterpret_cast<Base1*>(pb2)); // コンパイルは通るが不正。
        printf("dynamic_cast<Base1*>(pb2)     = 0x%p\n", dynamic_cast<Base1*>(pb2)); // OK。クロスキャストするには必須。
        puts("** Cross cast:");
        printf("reinterpret_cast<Base2*>(pb1) = 0x%p\n", reinterpret_cast<Base2*>(pb1)); // コンパイルは通るが不正。
        printf("dynamic_cast<Base2*>(pb1)     = 0x%p\n", dynamic_cast<Base2*>(pb1)); // OK。クロスキャストするには必須。
        puts("");
    }

    puts("Press any...");
    _getch();
    return 0;
}

上記コードの実行結果は、例えば

&d  = 0x003CFA50
pb1 = 0x003CFA50
pb2 = 0x003CFA54
** Down cast:
static_cast<Derived*>(pb1)      = 0x003CFA50
reinterpret_cast<Derived*>(pb1) = 0x003CFA50
dynamic_cast<Derived*>(pb1)     = 0x003CFA50
** Down cast:
static_cast<Derived*>(pb2)      = 0x003CFA50
reinterpret_cast<Derived*>(pb2) = 0x003CFA54
dynamic_cast<Derived*>(pb2)     = 0x003CFA50
** Cross cast:
reinterpret_cast<Base1*>(pb2) = 0x003CFA54
dynamic_cast<Base1*>(pb2)     = 0x003CFA50
** Cross cast:
reinterpret_cast<Base2*>(pb1) = 0x003CFA50
dynamic_cast<Base2*>(pb1)     = 0x003CFA54

のような感じになります(VC++ 2010 SP1で検証)。コメントに書いてる通りで、クロスキャストにはdynamic_castしか使ってはいけない、というのはまともなC++erであれば誰でも知ってるわけですが、

・ダウンキャストには可能な限りdynamic_castを使い、最悪でもstatic_castを使うべし(C言語のキャスト構文は厳禁)

というのはなかなか理解してもらえません。コード中でDerivedが、Base1, Base2の順に多重継承しているのがキーポイント。最初の親クラスであるBase1へのポインタからDerivedへのポインタを取り出すときは、一応reinterpret_castでもOKなわけですが(もちろんこの動作は実装依存なので規格では保証されない)、次の親クラスであるBase2へのポインタからDerivedへのポインタを取り出すときはreinterpret_castの使用は厳禁なわけです。

ちなみにreinterpret_castに限らず、C言語形式のポインタキャスト、例えば

(Derived*)&base;

みたいなのは、結局のところ

reinterpret_cast<Derived*>(&base);

と同じなので、上記のダウンキャストに潜む罠に思いっきり引っかかります。C++多重継承の場合、メモリーレイアウトとかの関係上、ダウンキャスト結果が継承した順番に左右されるため、そいつをきちんと考慮した上で適切なキャスト構文を使う必要があります。static_castはただの強制キャストではなく、クラス階層のナビゲーションってのをやってくれるので、(継承関係が保証されていれば)メモリーレイアウトを考慮して適切なポインタを返すようにしてくれます。自分は多重継承をわりとよく使う肯定派*1なので、こういうのはもっとまわりに啓蒙していかないと、自分の書いたコードを誰かが利用したりメンテしたりするときにハマってしまうおそれがありそうな気もします。

プログラムはとりあえずコンパイルが通って動いてりゃいいってもんじゃないわけですが、それを分かってくれるような良識のある開発者は少ないです。

ちなみにクロスキャスト(dynamic_cast)を使う場合は必ずRTTI(実行時型情報)が必要になります。個人的にはRTTIを切ったC++を使うのは推奨しませんが、パフォーマンス的な問題からRTTIをどうしても切る必要がある場合、そのソフトウェアの設計に使用可能なデザインパターンにも影響を与えるので十分注意しましょう。

*1:ただし自発的に書くコードでは、C#Javaの制限同様、抽象メソッドのみを含むinterfaceの多重継承に限ります。