次のコードを検討してください。
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
virtuallyはBase
から継承するため、コードはコンパイルされません。継承からvirtual
を削除すると、コードが有効になります。
このルールが存在する技術的な理由は何ですか?
技術的な問題は、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
が可能かどうかを確認せずに推測します。
[*]つまり、この例が要点を証明しない場合は、オフセットが異なる場合が見つかるまで、仮想継承を追加していきます;-)
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
はLeft
とRight
で構成され、eachには独自のBase
のコピーがあります。上記のすべてのクラスのメモリ構造はコンパイル時に既知であり、static_cast
は問題なく使用できます。
次に、同様の構造を検討しますが、Base
の仮想継承を使用します。
class Base {...};
class Left : public virtual Base {...};
class Right : public virtual Base {...};
class Bottom : public Left, public Right {...};
仮想継承を使用すると、Bottom
が作成されるときに、Base
のoneコピーのみが含まれ、sharedになります。 オブジェクト部分Left
とRight
の間。 Bottom
オブジェクトのレイアウトは、たとえば次のようになります。
Base part
Left part
Right part
Bottom part
ここで、Bottom
をRight
にキャストすることを検討してください(これは有効なキャストです)。 2つの部分からなるオブジェクトへのRight
ポインターを取得します。Base
とRight
の間には、(現在は無関係)Left
部分。このギャップに関する情報は、実行時にRight
(通常vbase_offset
と呼ばれる)の非表示フィールドに格納されます。詳細は here などで確認できます。
ただし、スタンドアロンのRight
オブジェクトを作成するだけでは、ギャップは存在しません。
したがって、Right
へのポインタのみを指定した場合、それがスタンドアロンオブジェクトなのか、それよりも大きなオブジェクト(Bottom
など)の一部なのかは、コンパイル時にわかりません。 Right
からBase
に正しくキャストするには、ランタイム情報を確認する必要があります。そのため、static_cast
は失敗し、dynamic_cast
は失敗しません。
dynamic_castに関する注意:
static_cast
はオブジェクトに関する実行時情報を使用しませんが、dynamic_cast
はそれを使用し、それが存在することを要求します!したがって、後者のキャストは、少なくとも1つの仮想関数(たとえば、仮想デストラクタ)を含むクラスでのみ使用できます。
基本的に、本当の理由はありませんが、意図はstatic_cast
が非常に安価であり、ポインタへの定数の追加または減算を最大で含むことです。そして、必要なキャストを安価に実装する方法はありません。基本的に、追加の継承がある場合、オブジェクト内のDerived
とBase
の相対位置が変わる可能性があるため、変換にはdynamic_cast
のかなりのオーバーヘッドが必要になります。委員会のメンバーは、これがstatic_cast
ではなくdynamic_cast
を使用する理由を打ち負かすだろうとおそらく考えていました。
次の関数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
static_cast
はコンパイル時の構成です。コンパイル時にキャストの有効性をチェックし、無効なキャストの場合はコンパイルエラーを出します。
virtual
ismは実行時の現象です。
両方一緒に行くことはできません。
この場合、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つだけです。つまり、メモリの連続領域を分離して、それを派生オブジェクトとして扱うことができるとは限りません。