Be Newsletter Issue 79, June 25, 1997より
BE ENGINEERING INSIGHTS:
壊れやすいベース・クラス(FBC)問題とは、何ですか?
By Peter Potrebic
我々はBeOSの実装を分離したり、隠したりしようと努力していますが、それでも開発者が我々のダイナミックロード・ライブラリに対してコンパイルし、リンクする時点である種の依存関係が生成されます。
これらの依存は、以下を含みます:
-
オブジェクト(すなわち構造体やクラス)のサイズ
-
(publicやprotectedである)「可視の」データまでのオフセット
-
vtableの存在とサイズ
-
vtable中の仮想関数へのオフセット
あなたのアプリがコンパイルされ、リンクされる時、それ[ダイナミックロード・ライブラリ]はこれらすべての情報を記録します。
これらのうちの何かがライブラリ中で変更された場合、コンパイルされたアプリはもはや動きません。
これが「壊れやすいベース・クラス」問題です。
間もなく皆さんのお手元に届くプレビュー・リリースで、我々は賭けをしています:
プレビュー・リリースは、BeOSの以降のリリースと上位互換性を持ちます。
果たして、この互換性は、どこまで維持出来るでしょう?
率直に言って、我々にも分かりません--我々は遠からずこれを使い尽くしてしまうでしょう。しかし、我々が壁にぶち当たった時点で、我々は自分たちのおかれた場所を再評価します。
上位互換性のゴールを達成【achieveの間違いだと思うのだが...】するために、我々は何らかの手段を講じなければなりませんでした。
あなたが独自のライブラリを意図しており、クライアントのコードを壊すこと無くライブラリを再リリースしたいのであれば、あなたは同じような手段に従う必要があるかもしれません。
明るい側面
我々が自分たちのFBC解決案を見る前に、互換性を壊すことなしに何が変更「可能」なのかを見てみましょう:
-
非仮想関数。 クラスは必要なだけの新たな非仮想[関数]をいくつでも採用することができます。
古いコードが新しい関数の利点を利用することはもちろん不可能ですが、あなたは何も壊しません。
-
新しいクラス。 新しいクラスは、それが既存のクラスの継承階層を変更しない限り導入可能です。
-
インプリメンテーション。 既存の関数のインプリメント方法は、変更しても構いません。
明らかに、古い関数を軽率に再インプリメントするべきではありませんが、それがFBC問題に影響を及ぼすことはありません。
暗い側面
ここではFBC問題に影響を及ぼすことがらと、それら個々の事象に対する我々の解決策を述べます:
* オブジェクトのサイズは、変更出来ません
「オブジェクトのサイズ」は、[そこに属する]個々のデータ・メンバーのサイズの合計を意味します。
多くのデータ・メンバーが加えられた場合、クラスは互換性を壊す可能性があります。
プレビュー・リリースで、我々は各クラスの中に「適切な」量のデータ[メンバー]を予約しました:
uint32 _reserved[X];
「X」はclass単位で決められました。
我々が今後決して変更が無いと予測したクラス(たとえばBRect)については、我々は一切の付け足しを行いませんでした。
オブジェクトが小さくても今後成長する可能性があれば、我々は少し−現在のサイズの25-50%程度−を付加しました。
例えば、BMessengerオブジェクトは、13バイトの実データを持ちます;
我々は、それに追加の7バイトを加えました。
大きいクラスは、より多くの追加がされています。
従って、今から3ヵ月後に「適切な量」が使い尽くされて、小さくなりすぎてしまったら、どうするのでしょう?
最後の「_reserved」値は、別の構造[体?]を指し示すために使うことが出来ます(ポインターとint32が同じサイズでない可能性を考慮に入れて)。
*Publicな可視データメンバーのオフセットは変更してはいけません
FBC問題を考える場合、C++の「protected」キーワードは実際には「public」を意味することを理解しなくてはなりません
いかなる「protected」なものでも、公開メンバーとして見ることが出来ます。
いかなる「public」や「protected」のデータ・メンバーも、厳密に固定されます:
それらのオフセットとサイズは、決して変えることができません。
これは本当[の意味で]は問題ではないので、「解決策」はありません;
ちょうど何かです ― あなたが知っているにちがいないことは ― あなたがあなた自身のライブラリを作っているかどうか。
* インライン関数に細心の注意を払ってください
インライン関数がprivateデータ・メンバーのサイズ/オフセットをさらすならば、そのprivateメンバーは実際にpublicです:
そのサイズとオフセットは、決して変えられません。
我々は、キットからこうしたインライン[関数]をすべて削除しました。
何らかのオーバーライドに伴うパフォーマンスの問題が無い限り、私はあなたも自身のライブラリ中で同じことをするように勧めます。
覚えておいてください:
唯一の安全なインライン[関数]は、publicメンバー(データや関数)や非仮想のprivateメンバー関数だけを参照するものです。
* VTableの現状と将来
classやstructが*この先*vtableを持つ可能性があるのであれば、今のうちに持っておくようにした方がよいでしょう。
この最初の仮想関数を加えることにより、クラスのサイズとおそらくあらゆる個々のデータ・メンバーのオフセットが変更されます。
ダミーの仮想[関数]を一つ(もしくは幾つか)加えておくことにより、永遠にあなたの平和が維持されます。
クラスが仮想関数を必要としなければ、あなたは何もする必要がありません。
たとえクラスがすでに仮想関数を持っていても、さらに付加したいかもしれません--今のうちにやっておくか、今後決してやらないかです。
Beキット中で、大部分のクラスは、付加的な予約済みの仮想関数を持っています;
あるBeヘッダ・ファイルのどんなprivateセクションについても、その先頭を見れば、そうしたものを見つけるでしょう:
class BWhatAPain {
public:
...
private:
virtual void _ReservedWhatAPain1();
virtual void _ReservedWhatAPain2();
virtual void _ReservedWhatAPain3();
...
};
* 醜いセーフティ・ネット
いくつかのクラスについては、余分な仮想関数がどれだけあれば適正かを評価することは難しいものです。
多すぎるのはOKですが、少なすぎるのは駄目です。
この問題を解決するために、追加の「ioctl」風の仮想関数をクラス階層に付加することができ、これにより無制限の(しかし醜い)伸張性を実現できます。
「Perform」は、この種類の関数に対して選んだ名前です。
例として、BArchivableクラスの中身を見てみましょう:
class BArchivable {
public:
...
virtual status_t Perform(uint32 d, void *arg);
};
この関数が必要であれば、我々は「セレクター・コード」を定義することができ、そのようにPerform関数を使うことができます:
ptr->Perform(B_SOME_ACTION, data);
これはきれいではありませんが、あるクラスがダミーの仮想関数を使い果たした場合に余裕を与えることができます。
* public仮想関数の順序は変えてはいけない
public仮想関数がヘッダ・ファイル中でpublic仮想関数が現れる順序は、決められています。
Metrowerksコンパイラは、vtableエントリをそれらの仮想関数が現れる順序に基づいて順序づけます。
仮想関数順序は、後に着けてあちこちに動かされることができません。(幸運にも、エントリはアルファベット順に並べられていません!これについて考えてみなさい。)
一方、private仮想関数は、並べ替えることが*出来ます*。
しかしこれは単に、我々がキット中でオーバーライド可能(必須)のprivate仮想関数を定義していないからにすぎません。
* ジレンマ
最後の2つの項目を見ることにより、不幸な問題に至ります。
今後のBeOSリリース中で、我々がダミー仮想関数の一つを使いたいくなった場合について言いましょう。
我々はそれを単純にヘッダ・ファイルの別のパートの方へ移動することはできません−-なぜならvtableの順序は、固定されているからです。
しかし、関数はprivateからpublicに動かすことが<可能>です。
我々が(Be社として)ダミー仮想関数を必要とすることになれば、我々は単純に最上段のprivate関数の「皮をむい」て、publicセクションに押し上げます。
例:
class BWhatAPain {
public:
...
virtual int32 NewDR10Function(... 引数リスト ...);
private:
virtual void _ReservedWhatAPain2();
virtual void _ReservedWhatAPain3();
...
};
これが、我々がダミー仮想関数をprivateセクションの最上部に張り付けることを選んだ理由です。
しかし、そのクラスが仮想関数を含むprotectedセクションを持っていたらどうするのでしょう?
覚えておいてください、あなたは自身の仮想関数を並び替えることは出来ませんが、セクションを「はさみ込む」ことは可能なのです。
これは、きれいではありません...
class BAreWeHavingFunYet {
public:
...
protected:
...
virtual int32 SomeOldProtectedVirtual();
public:
virtual int32 NewDR10Function(... arglist ...);
private:
virtual void _ReservedAreWeHavingFunYet2();
virtual void _ReservedAreWeHavingFunYet3();
...
};
...、しかし、これは動きます。
* 別のジレンマ
仮想関数のオーバーライドに関して、別の微妙な問題があります。
私は、すぐにこの問題を説明しますが、最初に解決策を述べておきます:
あるクラスが、将来的に継承された仮想関数をオーバーライドする必要が生じると予想されるなら、その[仮想]関数を*今*オーバーライドしておくことが望ましく、かつ単純になります。
問題は、ここにあります。
キットがこのように一組のクラスを宣言する場合について言いましょう:
class A {
public:
virtual X();
};
class B : public A
{ ... }; // つまり、BはA::X()をオーバーライド*しない*
今、開発者は彼ら自身のクラス(C)をBからの継承によって生成し、X()関数を以下のようにオーバーライドするとします:
C::X() {
...
inherited::X(); // 痛っ! 静的に決定された
A::X()に対する呼び出しだ
...
}
inheritedへの呼び出しは、仮想ではありません。
これは「最近接」オーバーライドに対する、静的に決められた呼び出しです;
この場合、それはA::X()として決定されます。
これは今のところOKですが、今後のリリースでクラスBがX()を*明示的に*オーバーライドしたらどうなるでしょう?
開発者のコードは、*依然として*inherited::X()をA::X()として決定します--言い換えると、開発者はB::X()を正しくスキップします。
あらゆる場面をカバーするための解決案は、全ての継承された仮想関数(インプリメンテーションが単にinheritedを呼び出す)を完全にオーバーライドすることです。
しかし、それはやりすぎです;
それは、パフォーマンスに影響を与えることになるでしょうし、我々のAPIを複雑にします。
従って、我々はキット中でいくらかの見切りを行いました;
ある関数をオーバーライドするクラスもあれば、しないものもあるわけです。
しかし、もしDR10のリリースも間近になって、我々が一連のオーバーライドする必要があるものは、という推測が間違っていたと理解してしまったらどうなるでしょう。【ここはちょっと訳に自信なし】
解決案はありますが、それは複雑で、*余りに*醜く、問題がありすぎてここでは説明できません。
* 他の雑多な項目
-
vtableを含むオブジェクトを決して共有メモリに入れてはいけません。
vtableは、特定のアドレス空間においてのみ有効なアドレスを含みます。
-
あなたは、new/delete演算子を後でオーバーライドすることができません。
今やるか、決してやらないかです。
それだけのことです!
|