ポインターのコレクションの代入演算子

[戻る]
DOSKOI-PANDA 2004/12/13(月) 21:42:20
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時間単位になりますが
識者の方、お教えいただけたら幸いです。

monkey 2004/12/13(月) 23:20:09
「仮想コンストラクタ」(自らのコピーコンストラクタによってオブジェクトを動的に
生成するメンバ関数)
と呼ばれるテクニックが使えます。
参考: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*"にしないと
# エラーを出したかも知れません。

DOSKOI-PANDA 2004/12/14(火) 00:19:44
monkey さん、ありがとうございます。

Prototype パターンを使って自己複製を行い、
コレクション毎にインスタンスを別に持つという
方法も考えました。これだと2重delete の問題はないし、
代入の右辺は const 性が保てます。が、インスタンスを
複数生成するというのが気に入らなくて廃案にしました。

しかし、スマートポインタを使わない方式としては
上記のやり方が最善かも知れません。
これ以上よい方法が見つからなければ、使わせていただき
ます。

monkey 2004/12/14(火) 09:01:49
>Prototype パターンを使って自己複製を行い、
>コレクション毎にインスタンスを別に持つという
>方法も考えました。これだと2重delete の問題はないし、
>代入の右辺は const 性が保てます。

デザインパターンは詳しくないのでウェブサイトでちょっと調べたところ、「仮想コン
ストラクタ」のテクニックと「Prototypeパターン」の本質は同じもののようですね。

undefined name 2004/12/14(火) 10:09:37
> デザインパターンは詳しくないのでウェブサイトでちょっと調べたところ、「仮想コン
> ストラクタ」のテクニックと「Prototypeパターン」の本質は同じもののようですね。

あえて言うなら「Prototypeパターン」は概念的な典型であり、
「仮想コンストラクタ」は言語におけるその実装の一形態だろうか。

PATIO 2004/12/14(火) 10:24:23
コレクション内部のデータを弄らせたくないのであれば、
仮想コンストラクタパターンが一般的でしょう。
内部データの保護を考えるとインスタンスを別にすると言うのが
一番安全ですからね。
処理コストを考えるとポインタのコピーで済ませたいところですが、
ポインタを渡した時点で内部データの安全性は0に等しいので
処理コストを考えるのか、安全性を考えるのかで二択になると思います。
例え、constポインタを渡すようにしてもconst_castされれば、
どうにでもなってしまうので結局は同じ事ですし。
安全性もOKで処理コストも少ないと言うのは難しいと思います。
ポインタの引渡しで外部では変更不可の実装が出来たとしても
その実装部分の処理コストが大きければ、意味がありませんから。
折衷案ですし、使う側のモラルの問題もありますが、
RefPtrというポインタ参照用のconstポインタで返す関数を作成し、
変更の必要があるときはそのクラスに実装しているMakeCloneで複製して
行いなさいと言う実装はしてみたことがあります。
最低限の処理コストである程度の安全性は保てます。
但し、使う側の意識が低いと穴だらけということになりますけれど。
あとは、SharedPtrとの併用ですかねぇ。
私の場合は自作しましたけれど。

PATIO 2004/12/14(火) 10:49:32
あらら、ちょっと勘違いしてました。
コレクションそのものの代入演算子なんですね。
そうなるとやっぱりMakeCloneすべきだと思います。
ポインタをコピーしたらコピー先で変更できてしまうので
不許可でしょうから。

後、考えらるとしたらSharedPtrのコレクションにして
コピー先のValuesには複製フラグでも設けておいて
複製フラグが立っていたら更新系の関数を軒並みエラーにするかな。
直接メンバーのポインタを触れないようにしておけば、
仕組み上はconst性は保てそうな気はします。

DOSKOI-PANDA 2004/12/14(火) 21:15:07
[[解決]]
PATIO さん、ども。

SharedPtr とは、boost::shared_ptr のことですね。
これを使う方がカッコよく、通っぽいのかなと思ってましたが、
安全性を考えると、深いコピーを行う方がよいのですね。
確かに1つのインスタンスを複数箇所から参照するのは
危ないですもんね。

今日、職場の方で monkey さんの方法で実装してみました。
メモリの問題もなく、代入右辺の const 性も保たれ
上々でありました。

ありがとうございました。

PATIO 2004/12/15(水) 11:04:33
すいません、自作のクラスと話が混ざってしまってました。
boost::shared_ptrでも良いと思います。
私がSharedPtrと書いているのは参照カウンタを持ったポインタクラスを
そのように(私が)呼んでいるのでそう書いてしまいました。
CZSharedPtrというクラスを自前でこさえていますので
ぽろっと出ちゃいました。

古いスレッドには追加発言できません。