Win2K VC++6.0 SP5 です。
一般的な要求なのですが、どうしたらよいか解りません。
みなさんはどのようにやっているかをお聞かせ下さい。
複数の型の値を統一的に扱いたいため、値の抽象クラスを定義しました。
#include <string>
class Value {
public:
virtual std::string getType() = 0;
virtual std::string toString() = 0;
virtual const char* c_str() { return toString().c_str(); }
virtual ~Value() {};
};
で、必要な型をラッピングしました。
class BoolValue : public Value {
public:
// 実装は略
private:
bool value;
}
class CharValue : public Value {
public:
// 実装は略
private:
std::string value;
}
class IntValue : public Value {
public:
// 実装は略
private:
int value;
}
で、これらの型をコレクションしたいと思い、基底クラスのポインタのコレクションを
定義しました。
ポインタを使用するとメモリ管理の問題が発生しますが、Valueのインスタンスは
ヒープ上に作成することをユーザに(ドキュメントで)義務づけ、delete はコレクショ
ンの
デストラクター内で行うことにしました。
#include <vector>
class Value;
class Values {
public:
Values() : values();
~Values() {
for (int i = 0; i < values.size(); ++i) {
delete values[i];
}
}
private:
std::vector<Value*> values;
};
で、このクラスに代入演算子を定義することを考えたのですが、
代入してしまうとコレクションの中身である、ポインターが指す
アドレスの内容を複数コレクションで共有することになり、
デストラクターで二重破壊をしてしまうことになります。
vector の要素を auto_ptr で持つという禁断の手法を採用する
ことも考えましたが、やはりやめにしました。
auto_ptr が代入時にポインターの所有権を移動するという部分
だけをパクればよいので、
Value& operator = (const Values& values_) {
values = values_.values;
values_.values.clear(); // 本来 values_.valuesの全要素を 0
クリアすべき
return *this;
}
とやりましたが、これはもちろん const なオブジェクトを変更
しようとしているのでコンパイルは通らないので、
Value& operator = (Values& values_);
と宣言しました。コピーコンストラクターも同様に
Values(Values& values_) : values(values_.values) {}
としました。
が、代入演算子やコピーコンストラクターの引数の const 性が
失われるため、Values をさらにコレクションしている class で
コンパイルエラーが発生するようになってしまいました。
(正確には stl のヘッダ内で)
ポインターのコレクションを作る上で、boost::shared_ptr と
いうものがあるらしいのですが、これはポインターを所有する
参照カウントが 0 になったときに内部のポインターを破壊する
そうで、ということは代入の右辺の const 性は保たれないこと
になります。
ポインターのコレクションで複数クラスのオブジェクトを一括管理
するなんて、ありふれた処理だと思うのですが、やっている人は
どのように実装しているのでしょうか?
職場でネットが参照できないため、レスは24時間単位になりますが
識者の方、お教えいただけたら幸いです。
「仮想コンストラクタ」(自らのコピーコンストラクタによってオブジェクトを動的に
生成するメンバ関数)
と呼ばれるテクニックが使えます。
参考:More Effective C++, 項目25:コンストラクターと非メンバー関数を仮想化する
// sample.
#include <iostream>
class Value
{
public:
virtual Value* clone() const = 0;
virtual void f() const = 0;
};
class BoolValue : public Value
{
public:
BoolValue* clone() const { return new BoolValue( *this ); }
void f() const { std::cout << Bool << std::endl; }
};
class CharValue : public Value
{
public:
CharValue* clone() const { return new CharValue( *this ); }
void f() const { std::cout << Char << std::endl; }
};
class IntValue : public Value
{
public:
IntValue* clone() const { return new IntValue( *this ); }
void f() const { std::cout << Int << std::endl; }
};
#include <vector>
class Values
{
std::vector< Value* > values;
public:
Values(){}
~Values()
{
for( std::vector< Value* >::size_type i = 0; i < values.size(); ++i ){
delete values[i];
}
}
Values( const Values& rhs )
{
for( std::vector< Value* >::size_type i = 0; i < rhs.values.size();
++i ){
values.push_back( rhs.values[i]->clone() );
}
}
Values& operator = ( const Values& rhs )
{
if( this != &rhs ){
for( std::vector< Value* >::size_type i = 0; i < values.size();
++i ){
delete values[i];
}
values.clear();
for( std::vector< Value* >::size_type i = 0; i < rhs.values.size
(); ++i ){
values.push_back( rhs.values[i]->clone() );
}
}
return *this;
}
void push_back( const Value& elem )
{
values.push_back( elem.clone() );
}
void f() const
{
for( std::vector< Value* >::size_type i = 0; i < values.size(); ++i ){
values[i]->f();
}
}
};
void test( Values& v )
{
Values tmp;
tmp.push_back( BoolValue() );
tmp.push_back( CharValue() );
tmp.push_back( IntValue() );
v = tmp;
}
int main()
{
Values v;
test( v );
v.f();
}
# VC++6.0だと派生クラスのclone関数の戻り値の型をValue*にしないと
# エラーを出したかも知れません。
monkey さん、ありがとうございます。
Prototype パターンを使って自己複製を行い、
コレクション毎にインスタンスを別に持つという
方法も考えました。これだと2重delete の問題はないし、
代入の右辺は const 性が保てます。が、インスタンスを
複数生成するというのが気に入らなくて廃案にしました。
しかし、スマートポインタを使わない方式としては
上記のやり方が最善かも知れません。
これ以上よい方法が見つからなければ、使わせていただき
ます。
>Prototype パターンを使って自己複製を行い、
>コレクション毎にインスタンスを別に持つという
>方法も考えました。これだと2重delete の問題はないし、
>代入の右辺は const 性が保てます。
デザインパターンは詳しくないのでウェブサイトでちょっと調べたところ、「仮想コン
ストラクタ」のテクニックと「Prototypeパターン」の本質は同じもののようですね。
> デザインパターンは詳しくないのでウェブサイトでちょっと調べたところ、「仮想コン
> ストラクタ」のテクニックと「Prototypeパターン」の本質は同じもののようですね。
あえて言うなら「Prototypeパターン」は概念的な典型であり、
「仮想コンストラクタ」は言語におけるその実装の一形態だろうか。
コレクション内部のデータを弄らせたくないのであれば、
仮想コンストラクタパターンが一般的でしょう。
内部データの保護を考えるとインスタンスを別にすると言うのが
一番安全ですからね。
処理コストを考えるとポインタのコピーで済ませたいところですが、
ポインタを渡した時点で内部データの安全性は0に等しいので
処理コストを考えるのか、安全性を考えるのかで二択になると思います。
例え、constポインタを渡すようにしてもconst_castされれば、
どうにでもなってしまうので結局は同じ事ですし。
安全性もOKで処理コストも少ないと言うのは難しいと思います。
ポインタの引渡しで外部では変更不可の実装が出来たとしても
その実装部分の処理コストが大きければ、意味がありませんから。
折衷案ですし、使う側のモラルの問題もありますが、
RefPtrというポインタ参照用のconstポインタで返す関数を作成し、
変更の必要があるときはそのクラスに実装しているMakeCloneで複製して
行いなさいと言う実装はしてみたことがあります。
最低限の処理コストである程度の安全性は保てます。
但し、使う側の意識が低いと穴だらけということになりますけれど。
あとは、SharedPtrとの併用ですかねぇ。
私の場合は自作しましたけれど。
あらら、ちょっと勘違いしてました。
コレクションそのものの代入演算子なんですね。
そうなるとやっぱりMakeCloneすべきだと思います。
ポインタをコピーしたらコピー先で変更できてしまうので
不許可でしょうから。
後、考えらるとしたらSharedPtrのコレクションにして
コピー先のValuesには複製フラグでも設けておいて
複製フラグが立っていたら更新系の関数を軒並みエラーにするかな。
直接メンバーのポインタを触れないようにしておけば、
仕組み上はconst性は保てそうな気はします。
PATIO さん、ども。
SharedPtr とは、boost::shared_ptr のことですね。
これを使う方がカッコよく、通っぽいのかなと思ってましたが、
安全性を考えると、深いコピーを行う方がよいのですね。
確かに1つのインスタンスを複数箇所から参照するのは
危ないですもんね。
今日、職場の方で monkey さんの方法で実装してみました。
メモリの問題もなく、代入右辺の const 性も保たれ
上々でありました。
ありがとうございました。
すいません、自作のクラスと話が混ざってしまってました。
boost::shared_ptrでも良いと思います。
私がSharedPtrと書いているのは参照カウンタを持ったポインタクラスを
そのように(私が)呼んでいるのでそう書いてしまいました。
CZSharedPtrというクラスを自前でこさえていますので
ぽろっと出ちゃいました。