web-dev-qa-db-ja.com

仮想継承が関係しているときにstatic_castを使用してダウンキャストできないのはなぜですか?

次のコードを検討してください。

struct Base {};
struct Derived : public virtual Base {};

void f()
{
    Base* b = new Derived;
    Derived* d = static_cast<Derived*>(b);
}

これは規格([n3290: 5.2.9/2]DerivedvirtuallyBaseから継承するため、コードはコンパイルされません。継承からvirtualを削除すると、コードが有効になります。

このルールが存在する技術的な理由は何ですか?

42
eran

技術的な問題は、Base*からBaseサブオブジェクトの開始とDerivedオブジェクトの開始の間のオフセットを計算する方法がないということです。

あなたの例では、Baseベースを持つクラスが1つしか見えないため、OKと表示され、継承が仮想であるとは無関係に表示されます。しかし、コンパイラは、誰かが別のclass Derived2 : public virtual Base, public Derived {}を定義したかどうかを認識しておらず、そのBaseサブオブジェクトを指すBase*をキャストしています。一般に、[*]、Derived2内のBaseサブオブジェクトとDerivedサブオブジェクト間のオフセットは、Baseサブオブジェクトと、最も派生した型がDerivedであるオブジェクトの完全なDerivedオブジェクト間のオフセットと同じではない場合があります。 Baseが事実上継承されているためです。

したがって、完全なオブジェクトの動的な型、およびその動的な型が何であるかに応じて、キャストしたポインターと必要な結果との間の異なるオフセットを知る方法はありません。したがって、キャストは不可能です。

Baseには仮想関数がないため、RTTIがないため、完全なオブジェクトのタイプを確認する方法はありません。 BaseにRTTIがある場合でもキャストは禁止されます(理由はすぐにはわかりません)が、その場合はdynamic_castが可能かどうかを確認せずに推測します。

[*]つまり、この例が要点を証明しない場合は、オフセットが異なる場合が見つかるまで、仮想継承を追加していきます;-)

37
Steve Jessop

static_castは、クラス間のメモリレイアウトがコンパイル時にわかっているキャストのみを実行できます。 dynamic_castは、実行時に情報をチェックできるため、キャストの正確性をより正確にチェックできるだけでなく、メモリレイアウトに関する実行時の情報を読み取ることができます。

仮想継承は、BaseDerivedの間のメモリレイアウトを指定するランタイム情報を各オブジェクトに入れます。次から次へですか、それとも追加のギャップがありますか? static_castはそのような情報にアクセスできないため、コンパイラは保守的に動作し、コンパイラエラーを発生させます。


詳細:

複雑な継承構造を考えてみましょう。多重継承により、Baseのコピーが複数あります。最も典型的なシナリオは、ダイヤモンドの継承です。

class Base {...};
class Left : public Base {...};
class Right : public Base {...};
class Bottom : public Left, public Right {...};

このシナリオでは、BottomLeftRightで構成され、eachには独自のBaseのコピーがあります。上記のすべてのクラスのメモリ構造はコンパイル時に既知であり、static_castは問題なく使用できます。

次に、同様の構造を検討しますが、Baseの仮想継承を使用します。

class Base {...};
class Left : public virtual Base {...};
class Right : public virtual Base {...};
class Bottom : public Left, public Right {...};

仮想継承を使用すると、Bottomが作成されるときに、Baseoneコピーのみが含まれ、sharedになります。 オブジェクト部分LeftRightの間。 Bottomオブジェクトのレイアウトは、たとえば次のようになります。

Base part
Left part
Right part
Bottom part

ここで、BottomRightにキャストすることを検討してください(これは有効なキャストです)。 2つの部分からなるオブジェクトへのRightポインターを取得します。BaseRightの間には、(現在は無関係)Left部分。このギャップに関する情報は、実行時にRight(通常vbase_offsetと呼ばれる)の非表示フィールドに格納されます。詳細は here などで確認できます。

ただし、スタンドアロンのRightオブジェクトを作成するだけでは、ギャップは存在しません。

したがって、Rightへのポインタのみを指定した場合、それがスタンドアロンオブジェクトなのか、それよりも大きなオブジェクト(Bottomなど)の一部なのかは、コンパイル時にわかりません。 RightからBaseに正しくキャストするには、ランタイム情報を確認する必要があります。そのため、static_castは失敗し、dynamic_castは失敗しません。


dynamic_castに関する注意:

static_castはオブジェクトに関する実行時情報を使用しませんが、dynamic_castはそれを使用し、それが存在することを要求します!したがって、後者のキャストは、少なくとも1つの仮想関数(たとえば、仮想デストラクタ)を含むクラスでのみ使用できます。

6
CygnusX1

基本的に、本当の理由はありませんが、意図はstatic_castが非常に安価であり、ポインタへの定数の追加または減算を最大で含むことです。そして、必要なキャストを安価に実装する方法はありません。基本的に、追加の継承がある場合、オブジェクト内のDerivedBaseの相対位置が変わる可能性があるため、変換にはdynamic_castのかなりのオーバーヘッドが必要になります。委員会のメンバーは、これがstatic_castではなくdynamic_castを使用する理由を打ち負かすだろうとおそらく考えていました。

2
James Kanze

次の関数fooについて考えてみます。

#include <iostream>

struct A
{
    int Ax;
};

struct B : virtual A
{
    int Bx;
};

struct C : B, virtual A
{
    int Cx;
};


void foo( const B& b )
{
    const B* pb = &b;
    const A* pa = &b;

    std::cout << (void*)pb << ", " << (void*)pa << "\n";

    const char* ca = reinterpret_cast<const char*>(pa);
    const char* cb = reinterpret_cast<const char*>(pb);

    std::cout << "diff " << (cb-ca) << "\n";
}

int main(int argc, const char *argv[])
{
    C c;
    foo(c);

    B b;
    foo(b);
}

実際には移植可能ではありませんが、この関数はAとBの「オフセット」を示します。継承の場合、コンパイラーはAサブオブジェクトを自由に配置できるため(最も派生したオブジェクトが仮想ベースctorを呼び出すことも忘れないでください!)、実際の配置は、オブジェクトの「実際の」タイプによって異なります。ただし、fooはBへの参照のみを取得するため、static_cast(コンパイル時に最大でオフセットを適用することで機能する)はすべて失敗します。

ideone.com(http://ideone.com/2qzQu)の出力:

0xbfa64ab4, 0xbfa64ac0
diff -12
0xbfa64ac4, 0xbfa64acc
diff -8
2
PlasmaHH

static_castはコンパイル時の構成です。コンパイル時にキャストの有効性をチェックし、無効なキャストの場合はコンパイルエラーを出します。

virtualismは実行時の現象です。

両方一緒に行くことはできません。

この場合、C++ 03標準§5.2.9/ 2および§5.2.9/ 9が関連します。

タイプが「cv1 Bへのポインター」の右辺値(Bはクラスタイプ)は、有効な標準であれば、DがBから派生したクラス(句10)である「ポインターからcv2 D」の右辺値に変換できます。 「ポインターからD」から「ポインターからB」への変換が存在し(4.10)、cv2はcv1と同じcv-qualificationまたはそれ以上のcv-qualificationであるそしてBはDの仮想基本クラスではない。 NULLポインター値(4.10)は、宛先タイプのNULLポインター値に変換されます。タイプ「cv1 Bへのポインター」の右辺値が、実際にはタイプDのオブジェクトのサブオブジェクトであるBを指している場合、結果のポインターはタイプDの囲んでいるオブジェクトを指します。それ以外の場合、キャストの結果は未定義です。

1
Alok Save

これは、仮想継承を持つクラスのメモリレイアウトが異なるためだと思います。親は子供間で共有する必要があるため、継続的に配置できるのはそのうちの1つだけです。つまり、メモリの連続領域を分離して、それを派生オブジェクトとして扱うことができるとは限りません。

1
RocketR