読者です 読者をやめる 読者になる 読者になる

C++に実行時の型なんてものはない

は嘘でした。k.inaba さんの指摘で修正。
と、まで書くと言い過ぎか?*1
また、オーバーロードされた演算子の動作を特別に考える必要もない。

  • 演算子オーバーロードした演算子はメンバ関数と同じ動きをする。
  • virtualでない演算子は、演算子を呼び出した対象オブジェクトの「変数の型」によって決定される
  • virtualな演算子は、演算子を呼び出した対象オブジェクトの「実行時の型」によって決定される
  • 要するに、メソッドと同じ
C++で演算子オーバーロードしたときの演算子決定基準について調べた - 矢野勉のはてな日記

ちょっと違う。
単に同じ動きをするだけでなく、同じものだ。
クラスに対しオーバーロードされた演算子はメンバ関数のシンタックスシュガーに過ぎない。
また、メンバ関数であるとは限らないので、メソッドと同じとすべきではない。

あと、少なくとも、C++は実行時の型情報を元にメンバ関数を呼び出しているわけではない。C++ではvptr(仮想関数テーブルへのポインタ)+vtbl(仮想関数テーブル)によって呼び出されるメンバ関数が決定されるだけ。(vptr+vtblを型情報と呼ぶには違和感がある、だってこれらからは「それ」が何の型であるかなんてわからないんだから!)
C++の仮想関数は動作が『対象オブジェクトの「実行時の型」によって決定される』、この動作は演算子オーバーロードがシンタックスシュガーである以上、仮想関数のそれとは動作を違えない。また、動作が静的に定まる非仮想関数も同様である。この動作はメンバではない関数でも同様である。

みんな、演算子オーバーロードを特別なものと思おうとする気がしているけれど、
C++の通常の関数と同じであると考えて差し支えない。
そうすると結論は1つになる。

オーバーロードされた演算子は通常の関数のシンタックスシュガーを提供する機能である

まずシンプルに。

class Value {
public:
    friend Value operator+ (const Value& lhs, const Value& rhs) throw();
    Value& operator+= (const Value& rhs) throw() {
        this->value_ += rhs.value_;
        return *this;
    }
    Value(int value) : value_(value) {}
    void p() const { printf("value:(%d)\n", value_); }
private:
    int value_;
};

Value operator+ (const Value& lhs, const Value& rhs) throw()
{
    Value result = lhs;
    return result += rhs;
}

Valueはint値を保持して加算を提供するクラス。実用性はない。
このクラスは、

int main()
{
    Value a(100);
    Value b(1000);
    
    a += b;
    
    a.p();
    
    a = a + b + b;
    
    a.p();

のような書き方ができる。
a.pはそれぞれ、

value:(1100)
value:(3100)

と出力される。

では、これと等価な構文を考えよう、

    Value a(100);
    Value b(1000);
    
    a.operator+=(b);
    
    a.p();
    
    a = operator+( operator+(a,b), b );
    
    a.p();

だ。
このきもい関数呼び出しを演算子で格好良くしようぜ! という機能が演算子オーバーロードだ。
やってることは、
即ち、

    a.add(b);
    
    a = add( add(a, b), b );

ってことだ。

では、演算子オーバーロードの真に恐れるべきところとはなんだろう?

それは、「演算子を真の意味で正しく保つことは難しい」ということだ。
a + bが可能であれば、ユーザは
a += bを欲するだろうし、a = a + bと同じ結果を欲するだろう。
また、
a - bや
a * bを求められるかもしれない。
外積や内積を持つクラスを定義したとき、operator*はどちらを意味するだろうか?
こういうことを考えることが、定義する側にとっても使う側にとっても難しい。
故に安易に抽象化を持ち込み、複雑性を発生させてはいけないってことだ。

やれやれ!

C++の場合では、同じスニペットから分かることは何もない。まったく。
...
かけ算したときにはなはだ気の効いたことをやってくれるかもしれないからだ。
...
そして,神よ救いたまえ、どこかで継承が行われていれば、コードが実際どこにあるのか自分でクラス階層をたどる必要があり
...
あなたは決してすべてをチェックしたか本当に確信することはできないのだ。(やれやれ!)
...
問題は、漏れのない抽象化は存在しないということだ。この点については「漏れのある抽象化の法則」で詳しく議論したので、ここでは繰り返さない。

http://local.joelonsoftware.com/mediawiki/index.php/間違ったコードは間違って見えるようにする

*1:typeidがあるじゃんよー、dynamic_castがあるじゃんよー、という意見があると思うが、これらはメンバ関数の動作に影響を及ぼさない