VCで、自作の基本クラスから派生させたいくつかのクラスがあり、
それらの集合を、基本クラスのポインタの配列で保持しています。
(MFCアプリですが、基本クラスはMFCの派生クラスではありません)
この集合の内容を、一つのバイナリファイル内に保存して、
次回実行時に復元する機能を持たせる必要があるのですが、
各要素を復元する際に、クラスの種類が必要になってきます。
それらのクラスの種類は、どのように書いておくべきなのでしょうか。
実行中の各要素のクラス判定は、dynamic_castやtypeidで行えるのですが、
typeidの文字列をファイルに保存してしまってよいものなのでしょうか。
(将来コンパイラのバージョンが上がっても変わらない?)
それとも、派生クラスの種類を表す数値でも別途一元管理しておいて、
その数値を保存しておくべきでしょうか?
一般に、固定的な長さでないレコードで構成されたストリームの場合、
1.ストリームからオブジェクトを読み込むには、その本体を
読み込む前に、それが何であるか判別できていなければならない。
のですね。
従って、オブジェクト種別IDなどがオブジェクト本体を読み込む直前、
ないし、オブジェクトの先頭で示されることが必要です。
また、この種別IDは、その基本クラスで保持されるべき情報と判定でき、
このような構造のクラスの場合、typeid等の出番はあまりありません
(IDで判別可能ですもんね)。
加えて、この様な種別IDはせいぜい数万種類程度あれば十分なので、
文字列で保持するのはかなりまぬけです。
処理速度とデータサイズを考えれば、16bitあれば十分ですね。
具体的には、CADなどの図形を保存/読み込みする場合などを想像すると、
わかりやすいかもしれません。
ありがとうございます。
以下の件について確認させていただきたいのですが、
> また、この種別IDは、その基本クラスで保持されるべき情報と判定でき、
ということは、このような場合は、
派生クラスの種類は基本クラスの中で一元管理
ということになりますでしょうか?
基本クラスがすべての派生クラスを知っていなければいけない
(派生クラスが増えるたびに基本クラスにも付け加えていく)
というのがちょっと変かもしれないとも思っていたのですが。
色々な考え方や実装方法がありますが、
こんな感じはどうでしょう。
//-----図形IDの定義-----
typedef enum eFIG_ID{
eFIG_ERR,
eFIG_POINT,
eFIG_LINE,
eFIG_RECT,
// ・・・
}eFIG_ID;
//-----図形基本クラス-----
class FIG{
eFIG_ID m_ID;//図形ID
// ・・・
protected: FIG(){
m_ID = eFIG_ERR;
}
protected: FIG( eFIG_ID ex_ID){
m_ID = ex_ID;
}
public:
eFIG_ID Get_ID(){ return m_ID;}
virtual BOOL Load( FILE& ex_File){
}
virtual BOOL Save( FILE& ex_File){
}
};
//-----図形クラス(点)-----
class FIG_POINT : public FIG
{
public: FIG_POINT()
: FIG( eFIG_POINT)
{
}
public:
virtual BOOL Load( FILE& ex_File){
FIG::Load( ex_File);
}
virtual BOOL Save( FILE& ex_File){
FIG::Save( ex_File);
}
};
//-----図形クラスの配列-----
class FIG_Ary : public CArray< FIG*>
{
FIG * FIG_Fctry( eFIG_ID ex_ID){//ファクトリ
switch( ex_ID){
case eFIG_POINT: return new FIG_POINT();
// ・・・
}
}
eFIG_ID Load_ID( FILE& ex_File){
return ( eFIG_ID)ex_File.ReadINT16();
}
void Save_ID( FILE& ex_File, eFIG_ID ex_ID){
ex_File.WriteINT16( ex_ID);
}
public:
void DeleteAll(){} // 全て破棄
BOOL Load( FILE& ex_File){
DeleteAll(); // 全て破棄
int NumItems = LoadItemCount();//アイテム数
while( NumItems--){
eFIG_ID id = Load_ID( ex_File);// ID load
FIG * Fig = FIG_Fctry( id);
Fig->Load( ex_File);
Add( Fig);
}
return TRUE;
}
BOOL Save( FILE& ex_File){
FIG * Fig;
int NumItems = GetCount();
SaveItemCount( NumItems);//アイテム数
for( int i=0;i<NumItems;i++){
Fig = ( *this)[ i];
Save_ID( ex_File, Fig->Get_ID());
Fig->Save( ex_File);
}
return TRUE;
}
};
MFCだと、似たようなことが、CObArrayのシリアライズで実現できますね。
http://msdn.microsoft.com/ja-jp/library/6bz744w8(v=vs.80).aspx
基本クラス(CObject)にオブジェクトの動的生成機構を持っていて、
それにより実現しています。
具体的にはファイル出力時にクラス名(文字列)を出力。
ファイル読み込み時に、
CRuntimeClass::FromName()
にて、クラス名(文字列)からランタイムクラス構造体を取得、
CRuntimeClass::CreateObject()
にて、クラスオブジェクトを動的生成します。
ありがとうございます。
どちらも大いに参考にさせていただきます。
> 仲澤@失業者さん
> //-----図形IDの定義-----
> typedef enum eFIG_ID{
> eFIG_ERR,
> eFIG_POINT,
> eFIG_LINE,
> eFIG_RECT,
> // ・・・
> }eFIG_ID;
この部分、FIGクラスのメンバm_IDを表す定数なので、
FIGクラス自身のヘッダに書きたくなりますが、
そうなるとやはり、基本クラスがすべての派生クラスを知っていることになります。
この手のクラス群をファイルに保存しようとすると、
やはりこういう仕組みになるものなのですかね。
> bunさん
> 具体的にはファイル出力時にクラス名(文字列)を出力。
> ファイル読み込み時に、
> CRuntimeClass::FromName()
> にて、クラス名(文字列)からランタイムクラス構造体を取得、
MFCのCObjectは、typeidと同じような文字列表現を使って、
読み込み時の派生クラスの種類を特定しているということですか。
CObjectは、自身からの派生クラスの種類を定義することなんてできませんし、
そうなるとこういう文字列の方法になるのですね。
MFC内部で使っている以上、MFCやVCのバージョンが上がっても
その文字列が変わることはないと保証されていることになるでしょうけど、
こっちの方法は、数値をどこかで一元管理しなくて済む代わりに、
一度その名前でリリースしてしまったら、
ソース内のクラス名すら変更することが難しくなってしまいますね。
数値も文字列も、一長一短ありますね。
>そうなるとやはり、基本クラスがすべての派生クラスを知っていることになります。
その認識は誤りですね(笑)。
1.「図形基本クラス」はこのenumの型は知っていますが、
それが「何であるか」といった「意味」は知りません。
当たり前ですが「図形基本クラス」内でははそれを判定に
利用すべきではありませんし、具象クラスを特定できません。
2.enumは「人間」が見てわかりやすいように定義されているだけです。
eFIG_POINTという定義でなく、eFIG_001と書かれていた場合や、
単なる整数だった場合を想像しましょう。
動作はまったく変わりませんが、人間の感覚的にこれが点であるという
意識はなくなることでしょう。
コード上の論理的依存関係と、人間が感覚的に連想する依存関係を
混同するべきではありません。
3.一見しただけではわかりませんが、掲載したコードの流儀であれば、
enumにエントリされない派生クラスも実装可能である点に気付くべきです。
このとき「図形基本クラス」のコードはコンパイルする必要がありません。
基本クラスが派生クラスをまったく知らないことの証明になるので
実際にやってみましょう(もちろん各クラスをファイルに分割してからですよ)。
4.提示した実装では「図形配列クラス」からはenumと具象クラスの関係を
知っていることになっていますが、当然、隠蔽して実装することも可能ですし、
各クラスから独立した関数にすることすら可能です(良く使う手です)。
「ファクトリメソッド」と呼ばれる典型的なパターンの簡略版ですね。
そうした場合、配列クラスも具象クラスが何であるかをまったく知らずに
実装できてしまいます(笑)。
このようなインデペンデントなファクトリは、具象クラスの構築が
関連の無い複数の場面で必要な場合の実装です。
例えば、ストリームからの構築と、ユーザー入力による構築、
クリップボードからのペーストによる構築があり、かつ、保管管理する
配列が(一時的な場合も含めて)異なるインスタンスってな場合ですね。
今回のご質問はファイルがらみだけでしたので極力簡略化するため、
配列クラスに実装しました。
私を含め、これまでの意見をひっくるめると、
要は派生クラスを判別するためのテーブルを基本クラスに持っている
ということです。
1) enum eFIG_IDだったり
2) CRuntimeClassだったり
そのテーブルへの登録が派生クラスの側からできれば、それでいいわけ
です。
1) FIG_POINT()
: FIG( eFIG_POINT)
2) IMPLEMENT_SERIAL()
その上で、テーブルからのクラスオブジェクトの動的生成ができること、
ここまでが条件ですね。
ただし、テーブルに登録する以上、全派生クラスでユニークとなるキーが
必要なことは理解できるでしょう。
それが enum値だったり、クラス名だったりしているわけです。
つまり、Zさんの要求を全て満たすためには、じゃあキーは何にする?
という問題に行き着くわけです。
キーは別に何でも良いわけですが、
結局のそのキー名は変えることができない。
そこはどうしようもない気がします。
Active-Xなんかだと、キーがUUIDだったりします。
(今回の話とはずれますが考え方としては同一延長線上かなと)
何度もすみません。
>> そうなるとやはり、基本クラスがすべての派生クラスを知っていることになります。
> その認識は誤りですね(笑)。
すみません、ちょっと表現がうまくなかったかもしれません。
「基本クラスのヘッダを書いた人が」という意味です。
> 要は派生クラスを判別するためのテーブルを基本クラスに持っている
> ということです。
開発していて派生クラスが増えるたびに、
基本クラスのヘッダに書かれているenumテーブルに追記していくのは、
汎用ライブラリでも作っていない限りは、考えられる実装方法ということですね。
bunさんがうまくまとめられている通り、本件の場合
1.「それ」を表す何らかの識別子は「原理的」に必要。
2.ストリーム上では、「それ」の先頭ないし本体の前に必要。
というわけで、
3.識別子の種別を無視すれば、原理的には誰が書いても似たようなコードになる。
わけですね。
>「基本クラスのヘッダを書いた人が」という意味です。
それだと都合が悪い場合は、IDをただの整数として定義し、
各IDを、以下の方法で個々の具象クラスに実装する。
4.const int FIG_POINT = 1000; // 点
5.#define FIG_POINT 1000 // 点
などの手法が使えます。この場合基本クラスを書いている人は
それが整数であることを超えるような知識は無用です。
別の共通ヘッダーでは、
enum eFIG_ID{
eFIG_ERR = -1,
eFIG_POINT = FIG_POINT, // 点
}FIG_ID;
として、運用すればよいわけですね(重複チェックにもなります)。
「クラスの種類」は、基本クラスや派生クラスの中で使われることはなく、
派生クラスへのポインターを格納するコンテナを読み書きするときに使われるだけのよ
うに思います。
となると、コンテナの下請け関数として
BaseClass* Create(int classID), int GetID(constBaseClass&)
のようなものを作り、この2つの関数を実装するcppファイルの中で「クラスの種類」を
まとめて記述するほうが良いように思います。