October 21, 2007

memcpyによるクラスのコピー

C++の話です。オリジナルのQueue、すなわちFIFO(First In First Out)といった方がわかりやすいでしょうか、を実装することになりました。基本的なアルゴリズムなので、車輪の再発明は極力避けるべきだ、というのが常識だと思いますが、STLがないDSP用のプログラムを書かなければならない為やむを得ずといったところです。実のところ、本人的にはこういう基礎アルゴリズムを実装するのは、勘を養うことにもなりますし結構楽しんでやってしまいました。

実装にあたって、当初はcharやintといった基本型程度しか使わないかなと思っていたので、FIFOへの書込み、読み出しはmemcpyを使って実装していました。ところが途中から汎用化欲がフツフツと沸き始め、ええぃ普通のクラスも管理できれば、いつかどこかで楽しい思いをできるに違いないと思えてきたのです。

そこで今回のお題なわけですが、果たして自分で作ったクラスのコピーをとるのはmemcpyでいいのでしょうか。答えを言ってしまうと、memcpyでは問題がおこる場合があります。実際に今回は2つの問題に遭遇しました。

まず第1のケースは、以下に示すコードのような、参照カウンタを利用してライトウェイトな実装になっている場合です。

class LightWeight{
    private:
        struct storage_t {
            int ref; ///< 参照カウンタ
            int value; ///< 何か値
            storage_t() : ref_count(0) {}
        };
        storage_t *content;
    public:
        LightWeight() : content(new storage_t()) {} ///< コンストラクタ
        ~LightWeight(){ ///<デストラクタ
            if(content && ((--(content->ref)) <= 0)){
                delete content;
            }
        }
        LightWeight(const LightWeight &obj){ ///< コピーコンストラクタ
            if(content = obj.content){(content->ref)++;}
        }
        LightWeight &operator=(const LightWeight &obj){ ///<代入演算子
            if(this != &obj){
                if(content && ((--(content->ref)) <= 0)){delete content;}
                if(content = obj.content){(content->ref)++;}
            }
            return *this;
        }
        int &content(){return (content ? (content->value) : *(int *)NULL);}
};

このようなLightWeightクラスをコピーする場合は、代入演算子か、コピーコンストラクタによるコピーで参照カウンタの整合性を確保してやる必要があります。もしmemcpyで単純にコピーをすると参照カウンタが本来参照されている数よりも少なくなってしまい、Segmentation Faultが発生してしまいました。

遭遇した第2のケースですが、継承関係があり仮想関数が使用されている場合です。同じくコードで示します。

#include <iostream>

using namespace std;

class Base{
    public:
        virtual void hoge(){cout << "Base::hoge" << endl;}
        void ada(){hoge();}
};
class Sub : public Base{
    public:
        virtual void hoge(){cout << "Sub::hoge" << endl;}
};
int main(){
    Sub sub;
    Base base;
    base = sub;
    base.ada(); ///< (1)
    memcpy(&base, &sub, sizeof(Base));
    base.ada(); ///< (2)
    base = sub;
    base.ada(); ///< (3)
}

memecpyと代入演算子(=)の役割が同じであるならば、(1)、(2)、(3)、いずれも同じ出力が得られるはずですが、実はそうなりません。(1)では"Base::hoge"が、(2)と(3)では"Sub::hoge"が出力されます。これは仮想関数を成り立たせる仕組みの仮想関数テーブル(詳細はGoogle先生『仮想関数テーブル』)がコピーされるかどうかの違いで、本来はコピーされるべきではない仮想関数テーブルまでもがmemcpyではコピーされてしまうためにおきた問題です。例で示したコードだとあまり深刻さが伝わらないですが、子クラスで定義されたメンバ変数にアクセスする仮想関数がある場合などはおかしなことが起きると思います。

他にもmemcpyを使うと問題がおこるケースがあると思いますので、結局のところ、実装したFIFOでは、memcpyを使うか、代入演算子を使うか、あるいはもっと他の方法を使うか、ファンクタで選べるようにしました。拙いコードfifo.hをおいておきます。

23:27 fenrir が投稿 : 固定リンク | | このエントリーを含むはてなブックマーク | この記事をdel.icio.usでブックマーク | トラックバック
このエントリーのトラックバックURL: http://fenrir.naruoka.org/mt/mt-tb.cgi/597
コメント
コメントする









名前、アドレスを登録しますか?
(次回以降コメント入力が楽になります)
  • 匿名でのコメントは受け付けておりません。
  • 名前(ハンドル名可)とメールアドレスは必ず入力してください。
  • メールアドレスを表示されたくないときはURLも必ず記入してください。
  • コメント欄でHTMLタグは使用できません。
  • コメント本文に日本語(全角文字)がある程度多く含まれている必要があります。
  • コメント欄内のURLと思われる文字列は自動的にリンクに変換されます。