メモ帳

やったことや思いついたことをメモする帳。技術系メイン

【C++】演算子オーバーロード時の暗黙的アップキャスト

C++では、演算子をオーバーロードしてクラスの使い勝手をよくしたりすることができますが、 継承と一緒に使う時に発生したアップキャストに結構混乱した上まだわかってないところがあるのでメモ。

以下は、coordクラスとそれを継承したnyanクラスを利用して、オーバーロードの働き方を示すサンプルです。 nyanは特に意味のない、hogeとかpiyoみたいなものです。せっかくなので好きなものにしています。

ソースコード

#include <iostream>
using namespace std;

class coord
{
public:
    coord(){x = 0, y = 0;}
    coord(int i, int j){x = i, y = j;}

    coord operator+(const coord ob2);
    coord operator=(const coord ob2);
    
    int x, y; // coordinates
};

coord coord::operator+(const coord ob2)
{
    cout << "coord operator+(coord, coord)" << endl;
    coord tmp;
    
    tmp.x = x + ob2.x;
    tmp.y = y + ob2.y;
    
    return tmp;
}

coord coord::operator=(const coord ob2)
{   
    cout << "coord operator=(coord, coord)" << endl;
    
    x = ob2.x;
    y = ob2.y;
    
    return *this;
}

class nyan : public coord
{
public:
    nyan() : coord(0, 0){}
    nyan(int i, int j) : coord(i, j){}
    nyan operator=(const coord ob2);
    //nyan operator=(const nyan ob2);
};

nyan nyan::operator=(const coord ob2)
{
    cout << "nyan operator=(nyan, coord)" << endl;
    x = ob2.x;
    y = ob2.y;
    
    return *this;
}
/*
nyan nyan::operator=(const nyan ob2)
{
   cout << "nyan operator=(nyan, nyan)" << endl;
   
   x = ob2.x;
   y = ob2.y;
   
   return *this;
}
*/
int main()
{
    cout << "nyan o1(5, 5), o2(-1, 4), o3" << endl << endl;
    nyan o1(5, 5), o2(-1, 4), o3;

    cout << "# o3 = o1 + o2" << endl;
    o3 = o1 + o2;
    cout << "X: " << o3.x << ", Y: " << o3.y << endl << endl;
    
    cout << "# o3 = o1" << endl;
    o3 = o1;
    cout << "X: " << o3.x << ", Y: " << o3.y << endl;

    return 0;
}

出力結果

nyan o1(5, 5), o2(-1, 4), o3

# o3 = o1 + o2
coord operator+(coord, coord)
nyan operator=(nyan, coord)
X: 4, Y: 9

# o3 = o1
coord operator=(coord, coord)
X: 5, Y: 5

# o3 = o1 + o2

まず、演算子の順序から、o1 + o2の計算を行います。 o1, o2共にnyanクラスですが、(nyan, nyan)に対応する演算子がないので、暗黙的にアップキャストされて(coord, coord)の演算子が使われます。

次に、o3 = (o1 + o2)の計算が行われます。 o3はnyanクラス、(o1 + o2)は演算子の返り値より、coordクラスです。 したがって、(nyan, coord)の演算子が使われます。

# o3 = o1

o3, o1共にnyanクラスですが、(nyan, nyan)に対応する演算子がないので、暗黙的にアップキャストされて(coord, coord)の演算子が使われます。

ここで、なぜ(nyan, coord)ではなく、(coord, coord)が使われるのかが分かりません。

上記のコードのコメントを解除し、(nyan, nyan)対応の演算子を作成すると、

# o3 = o1
nyan operator=(nyan, nyan)
X: 5, Y:5

のように、(nyan, nyan)対応の演算子が優先されます。

優先順位としては、

  1. (nyan, nyan)
  2. (coord, coord)
  3. (nyan, coord)

のようになっているようです。

(nyan, nyan)は型にピッタリ適合しているから優先されるのはいいとして、残りの二つはどういう理由で選択されているのかが分かりません……。最適化?ウーン

ちなみに、ここで使わないからとうっかり(nyan, coord)を消してしまうと、先ほどのo3 = (o1 + o2)のときに(nyan, coord) がなくてエラーに引っかかります。

nyan は coordで代用できますが、coord は nyanで代用できません。


派生クラスは基底クラスで代用できますが、基底クラスは派生クラスで代用できません。


ということですね。

それ自体は当たり前のようですが、うっかり派生クラスの演算子を書き忘れた時に、コンパイラが気を利かして基底クラスの演算子を活用してしまったばっかりにバグに気付かない、なんてことが普通に起こりそうで怖いです。

今回は演算子を例に出しましたが、関数でもおそらく同じことが起こります。 継承は慎重に使う必要がありますね。

最後に話はそれますが、typeinfoヘッダを使って実行時に型を確認することもできます。

が、typeid(T).name()を使ってみたところ、出力結果にゴミが入ってたのでカットしました。 検索するとメモリリークの問題があるみたいなので、その関連かもしれません。