March 14, 2008

operator=()と継承

拙作のC++行列ライブラリmatrix.hのメンテナンスを久しぶりにやってみているのですが、部分行列の代入の解釈で少し考えることがありました。簡易的なコードで書くと以下のような問題です。

Matrix m1, m2;
m.partialMatrix = m2;

このとき部分行列への代入は、元の行列の書き換えもおこなうべき(上の例でいうとm1も変更されて然るべき)であると思います。現在の実装では行列を構成する要素データ、そして行列という箱、これら2つを別々に管理するフライウェイトパターンを用いていました。そのため代入については要素ごとの代入を行わず中身全体をつけかえる動作をしており、上記のような元の行列まで書き換えるということを想定していませんでした。

そこで部分行列を適当な子クラスで表現し、子クラスで代入演算子のオーバーロードを行うことによって、上記の問題の解決をはかろうとしました。つまり部分行列への代入は要素ごとの代入を行う一方、それ以外の行列では要素をまとめて付け替えるだけというフライウェイトパターンの共存を目指しました。

前置きがながくなってしまいましたが、ここで表題にあるとおりの代入演算子の問題に引っかかりました。どうも代入演算子を継承先でオーバーロードした際、そのオーバーライド(言葉がややこしいですね、笑)が通常の関数と異なるようなのです。違いを明らかにするため、検証用のプログラムoperator_equal_test.cppを書いてみました。要約をすると問題が生じるのは以下のようなケースでした。

class A {
    public:
        A(){}
        virtual ~A() {}

        self_t &operator=(const A &a){
            cout << "A::operator=(A)";
            return *this;
        }
};

class B : public A {
    public:
        B() : A() {}
        ~B() {}
        self_t &operator=(const A &a){
            cout << "B::operator=(A)";
            return *this;
        }
};

int main(){
    A a;
    B b;

    cout << "a = a => "; a = a; cout << endl; // (1)
    cout << "a = b => "; a = b; cout << endl; // (2)

    cout << "b = a => "; b = a; cout << endl; // (3)
    cout << "b = b => "; b = b; cout << endl; // (4)

    return 0;
}

さて結果はどうなるでしょうか。(1)、(2)ではAが左辺オペランド、(3)、(4)ではBが左辺オペランドなので通常の関数と同様に考えるのであれば、(1)、(2)ではA::operator=(A)が、(3)、(4)ではB::operator=(A)が呼び出されそうなものです。しかし(4)では意表をついてA::operator=(A)が呼ばれます。VC2005/2008 Express Edition、gcc 3.4.4で試して同一の結果がでたので、おそらくC++の仕様でないかと思います(現在調査中ですが、ご存知でしたら是非お教えください)。なお(4)のケースでA::operator=(A)を呼ばれないようにするためには、B::operator=(B)なる関数を定義する必要がありました。もし、さらに深い継承関係がある場合には、親クラス::operator=(親クラス)で子クラス=子クラスを捕捉することも可能です。

以上のような検証の末、できあがったのが行列ライブラリmatrix.h(1.28)です。まだまだ改良の余地がありそうですが、よろしければ使ってみてください。

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

どうもご無沙汰です。ベンチプレスが挙がらなくなったホークスファンです。(わかるかな)
別件でググっていたところ奇跡的にたどり着いたのでふと読んでみました。

さて本題ですが、これはシンプルに B::operator=(B) が自動で生成されて、そのなかで A::operator=(A) が呼ばれているのではないかと思いました。
ま、勘違いだったらなかったことにしてください。

では仕事にもどるとします。

Posted by: soleus : April 11, 2008 03:18 PM

>soleusさん、改めTさん
お久しぶりです。お元気ですか? こちらの方は今年度に入ってからジムの年間パスを買う仲間が増えて嬉しい限りです。でもあの記録を打ち破れる人はでそうにないのでご安心ください(笑)
んで本題ですが、確かにそう考えると納得がいきますね。そうするとサブクラス内で自動生成されるoperatpor=は、サブクラス内で定義されたデータメンバのみのコピー、並びに親クラスのoperator=を呼び出している、と考えられますね。ありがとうございます。

Posted by: fenrir : April 12, 2008 10:37 AM

代入演算子のオーバーロードについて調べていて辿り着きました。
派生クラスでオーバーライドする場合、通常は基底クラスのメソッドはvirtualにすると思います。
記事のサンプルコードでは、基底クラスでvirtualになっておらず、別メソッドとして実装されているためそのような動作になっていると思います。
こちらで試したところ、基底クラスでvirtual指定すると、(4)のケースでoperator=(B)が呼ばれました。

以上、気になりましたのでコメントしました。

Posted by: 通りすがり : February 24, 2009 12:22 PM

>通りすがりさん
コメントありがとうございます。『virtualがない』ということで、なるほど、と思いこちらても追試をしてみました。しかしながら結果は変わらず、(4)のケースでもA::operator=(A)が呼ばれてしまいました(なおダウンロードできるコードに倣ってself_tについてはclass Aでtypedef A self_t;、class Bではtypedef B self_t;としています)。試した環境はVisual Studio Express Edition 2008とgcc 3.4.4についてです。よろしければテストされた環境を教えていただけないでしょうか。

Posted by: fenrir : February 25, 2009 09:05 AM

こちらにアップされているサンプルコードを見てみました。
そして分かったのは、class Aとclass Bで使用されているself_tの型がclass AではAでclass Bでは Bになっています。これだとコンパイル時に別メソッドと見なされるのでオーバーライドされないですね。
class Bでも戻り値をAにしてやるとオーバーライドされると思います。

Posted by: 通りすがり : February 25, 2009 12:28 PM

>通りすがりさん
おっしゃるとおりだと思います、お手数おかけしています。
しかしここではB::operator=の戻り値の型はBであって欲しいという都合がありまして…。例えばsome_functionがclass Bのみで定義されていて、(b=b_another).some_function()、および(b=a).some_function()(注:こちらは内部的に明示アップキャストをすることによって処理)をする場合が考えられます。大人しくB::operator=(B)関数を定義しておけば解決するのですが、僕の感覚が少しずれているのか、気持ち悪いなぁ、というのがこの記事の内容でした。

Posted by: fenrir : February 25, 2009 03:21 PM

なるほど。
もともとオーバーライドしていたわけではなかったのですね。

もうお気付きのようですが、
上でsoleusさんが言われているように
デフォルトではA::operator=(A)とB::operator=(B)が作られます。
そしてb = bをした場合、A::operator=(A)とB::operator=(B)の両方が呼ばれます。
これは、「A::operator=(A)だけが呼ばれているわけではない」
ということです。
※A::operator=(A)が呼ばれないと、Aのメンバは代入されませんよね?B::operator=(B)ではBのメンバのみ代入されます。

サンプルコードでは、たまたまA::operator=(A)のみオーバーロードしていたため、こちらに気付いただけで、fenrirさんの感覚が特にずれているわけではありません。B::operator=(B)もちゃんと呼ばれています^^
b = bでaの参照が最終的に帰るわけではないので、(b = b)としてもちゃんとclass Bのメソッドは使えるはずです。

ちなみにB::operator=(A)は自動で作られないので、自分で定義しない限り呼ばれません。
※未定義の状態でb = aをするとコンパイルが通りません。BはAですが、AはBではありません。

少々くどい説明になってしまい申し訳ありません(汗)

Posted by: 通りすがり : February 26, 2009 12:58 AM
コメントする









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