47

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

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

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

これは標準 ( ) で禁止されているため、実質的にから継承される[n3290: 5.2.9/2]ため、コードはコンパイルされません。を継承から削除すると、コードが有効になります。Derived Basevirtual

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

4

6 に答える 6

41

技術的な問題は、サブオブジェクトの開始点とオブジェクトBase*の開始点の間のオフセットを特定する方法がないことです。BaseDerived

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

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

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

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

于 2011-09-20T12:30:05.970 に答える
14

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

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


さらに詳細に:

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

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

このシナリオでは、とでBottom構成され、それぞれに の独自のコピーがあります。上記のすべてのクラスのメモリ構造はコンパイル時に認識され、問題なく使用できます。LeftRightBasestatic_cast

同様の構造を考えてみましょうが、 の仮想継承がありBaseます。

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

仮想継承を使用すると、Bottomが作成されたときに、オブジェクト パーツとの間で共有される のコピーが1 つだけ含まれるようになります。オブジェクトのレイアウトは、たとえば次のようになります。BaseLeftRightBottom

Base part
Left part
Right part
Bottom part

Bottomここで、キャストすることを検討してくださいRight(これは有効なキャストです)。Right2 つの部分に分かれているオブジェクトへのポインターを取得します。その間に、(現在は無関係な)部分を含むメモリ ギャップがありますBase。このギャップに関する情報は、実行時に(通常は と呼ばれる) の非表示フィールドに格納されます。たとえば、ここで詳細を読むことができます。RightLeftRightvbase_offset

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

したがって、ポインタだけを示しRightた場合、それがスタンドアロン オブジェクトなのか、それともより大きなオブジェクトの一部なのか (たとえば ) はコンパイル時にわかりませんBottomRightからに正しくキャストするには、ランタイム情報を確認する必要がありますBase。それがstatic_cast失敗する理由であり、失敗しdynamic_castない理由です。


dynamic_cast に関する注意:

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

于 2017-06-09T21:30:31.577 に答える
2

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

于 2011-09-20T12:55:10.353 に答える
1

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

virtualism はランタイム現象です。

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

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

タイプ「cv1 B へのポインター」の右辺値 (B はクラス タイプ) は、タイプ「cv2 D へのポインター」の右辺値に変換できます。ここで、D は B から派生したクラス (第 10 節) です。 「D へのポインター」から「B へのポインター」への変換が存在し (4.10)、 cv2 が cv1 と同じ cv 修飾であるか、cv1 より大きい cv 修飾であり、 B が D の仮想基底クラスではない。null ポインター値 (4.10) は、変換先の型の null ポインター値に変換されます。型「cv1 B へのポインター」の右辺値が実際には型 D のオブジェクトのサブオブジェクトである B を指している場合、結果のポインターは型 D の外側のオブジェクトを指します。それ以外の場合、キャストの結果は未定義です。 .

于 2011-09-20T12:15:44.930 に答える
1

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

于 2011-09-20T12:26:52.423 に答える