発生している問題は、ストレージの割り当てに関係しています。配列が割り当てられるとき、配列にはすべての要素のストレージが含まれている必要があります。(非常に単純化された)例を挙げましょう。次のように設定されたクラスがあるとします。
class Base
{
public:
int A;
int B;
}
class ChildOne : Base
{
public:
int C;
}
class ChildTwo : Base
{
public:
double C;
}
を割り当てるとBase[10]
、配列内の各要素には (一般的な 32 ビット システム* で) 8 バイトのストレージが必要になります。これは、2 つの 4 バイト int を保持するのに十分です。ただし、ChildOne
クラスには、その親の 8 バイトのストレージに加えて、そのメンバー用にさらに 4 バイトが必要ですC
。クラスには、その親のChildTwo
8 バイトに加えて、そのdouble C
. これら 2 つの子クラスのいずれかを、8 バイトに割り当てられた配列にプッシュしようとするとBase
、ストレージがオーバーフローしてしまいます。
ポインターの配列が機能する理由は、それらが何を指しているかに関係なく、一定のサイズ (32 ビット システムではそれぞれ 4 バイト) であるためです。a へのポインターBase
は、 a へのポインターと同じですがChildTwo
、後者のクラスは 2 倍のサイズです。
演算子を使用すると、dynamic_cast
タイプセーフなダウンキャストを実行して を に変更できるBase*
ためChildTwo*
、この特定のケースで問題が解決します。
または、次のようなクラス レイアウトを作成することで、データ ストレージ (戦略パターン)から処理ロジックを切り離すことができます。
class Data
{
public:
int A;
int B;
Data(HandlerBase* myHandler);
int DoSomething() { return myHandler->DoSomething(this) }
protected:
HandlerBase* myHandler;
}
class HandlerBase
{
public:
virtual int DoSomething(Data* obj) = 0;
}
class ChildHandler : HandlerBase
{
public:
virtual int DoSomething(Data* obj) { return obj->A; }
}
このパターンは、 のアルゴリズム ロジックが、DoSomething
多数のオブジェクトに共通する重要なセットアップまたは初期化を必要とする可能性がある場合 (およびChildHandler
構築で処理できる場合) に適していますが、普遍的ではありません (したがって、静的メンバーには適していません)。 . その後、データ オブジェクトは一貫したストレージを維持し、操作を実行するために使用されるハンドラー プロセスをポイントし、何かを呼び出す必要がある場合に自身をパラメーターとして渡します。Data
この種のオブジェクトは一貫した予測可能なサイズを持ち、配列にグループ化して参照の局所性を維持できますが、通常の継承メカニズムのすべての柔軟性も備えています。
ただし、ポインターの配列に相当するものをまだ構築していることに注意してください。それらは、実際の配列構造の奥深くにある別のレイヤーに配置されているだけです。
* 細かいことを言う人へ: はい、記憶領域の割り当てに指定した数値は、クラス ヘッダー、vtable 情報、パディング、およびその他の潜在的なコンパイラに関する多数の考慮事項を無視していることに気付きました。これは網羅的なものではありませんでした。
パート II の編集: 以下の資料はすべて誤りです。私はそれをテストせずに頭のてっぺんから投稿し、2 つの無関係なポインターを reinterpret_cast する機能と、2 つの無関係なクラスをキャストする機能を混同しました。私の過ちを指摘してくれた Charles Bailey に感謝します。
配列からオブジェクトを強制的に取得し、それを別のクラスとして使用することはできますが、オブジェクト アドレスを取得し、ポインタを新しいオブジェクト型に強制的にキャストする必要があり、理論上の目的を無効にします。ポインターの逆参照を回避します。いずれにせよ、私の当初の主張 -- これは最初にしようとしているのは恐ろしい「最適化」です -- は今でも有効です。
編集:さて、あなたの最新の編集で、あなたが何をしようとしているのか理解できたと思います. ここで解決策を提供しますが、すべての聖なる愛のために、これを本番コードで決して使用しないことを誓ってください。これはエンジニアリングの好奇心であり、良い習慣ではありません。
ポインターの逆参照を回避しようとしているようですが (おそらくパフォーマンスのマイクロ最適化として?)、オブジェクトに対してサブメソッドを呼び出す柔軟性が必要です。基本クラスと派生クラスが同じサイズであることが確実にわかっている場合、これを知る唯一の方法は、コンパイラによって生成された物理クラスのレイアウトを調べることです。コンパイラは、必要に応じてあらゆる種類の調整を行うことができるからです。 、そして仕様はあなたに何の保証も与えません - そして reinterpret_cast を使用して、親を配列内の子として強制的に扱うことができます。
class Base
{
public:
int A;
int B;
void DoSomething();
}
class Derived : Base
{
void DoSomething();
}
void DangerousGames()
{
// create an array of ten default-constructed Base on the stack
Base items[10];
// force the compiler to treat the bits of items[5] as a Derived,
// and make a ref
Derived& childItem = reinterpret_cast<Derived>(items[5]);
// invoke Derived::DoSomething() using the data bits of items[5],
// since it has an identical layout
childItem.DoSomething();
}
これにより、ポインターの逆参照が節約され、パフォーマンスが低下することはありません。reinterpret_cast はランタイム キャストではないためです。これは本質的にコンパイラのオーバーライドであり、「あなたが何を知っていると思っていても、私は自分が何をしているのか知っています。黙って、やれ。" 「わずかな欠点」は、コードが非常に脆弱になることです。これは、またはコンパイラによって開始されたかどうかに関係なく、Base
またはのレイアウトに変更を加えるとDerived
、全体が炎上してクラッシュする可能性が高いためです。非常に微妙で、未定義の動作をデバッグすることはほとんど不可能です。繰り返しますが、これを本番コードで使用しないでください。最もパフォーマンスが重要なリアルタイム システムでさえ、ポインター逆参照のコストは常にコードベースの真ん中に核爆弾に相当するものを構築するよりも価値があります。