自作の型を組み込み型のように扱う
std::string では連結に a + b、出力に cout << s と書けることはすでにご存じでしょう。これらはコンパイラの特別な仕掛けではなく、変わった名前を持つただの関数です。演算子オーバーロード とは、あなたの クラスを同じ構文に接続できるようにする機能で、これにより Vector2 や Money といった型を、int とまったく同じように加算したり、比較したり、出力したりできます。
仕組みは、一度わかってしまえば単純です。a + b のような式は省略形です。コンパイラはこれを operator+ という名前の関数呼び出しに書き換え、オペランドの型に一致するものを探します。その関数を自分のクラス向けに定義すれば、突然 a + b が動くようになります。これは実は関数オーバーロードの特殊な形であり、そこでの名前解決の規則がそのまま当てはまります。ただし名前が演算子の形をしているだけです。
この関数が両方のオペランドを const& で受け取っている点に注目してください。算術演算は入力を変更すべきではなく、参照を使えばコピーも避けられます。そして 新しい Vector2 を値で返します。p + q は、2 + 3 が 2 を変えないのと同じように、p や q に触れずに新しい結果を生み出さなければなりません。
メンバ vs 非メンバ
演算子を定義できる場所は2つあります。クラスの メンバ として定義するか、自由(非メンバ)関数 として定義するかです。メンバとして定義すると、左オペランドは暗黙の this になるため、二項演算子は明示的な引数を1つだけ取ります。
引数リストの後ろの const は重要です。a + b は a を変更すべきではないので、メンバを const としてマークします。本質的に左オペランドに結び付いていて、その左オペランドに変換が不要な演算子には メンバ形式 を使います。+=、[]、()、->、そして -x や ++x のような単項演算子がそれにあたります。
メンバの落とし穴は、左オペランドを変換できないことです。上記のメンバ operator+ では a + 50 は動きます(右辺向けに 50 が Money に変換される)が、50 + a は コンパイルできません。左オペランドの 50 は int であり、int にメンバ関数を追加することはできないからです。非メンバ 演算子ならこれを解決できます。両方の オペランドが明示的な引数になり、どちらも変換できるからです。
経験則: 対称的な二項演算子(+、==、*)は 非メンバ にして両側で変換が効くようにし、左オペランドを変更しなければならない、あるいは左オペランドに結び付いている演算子(+=、[]、=)は メンバ にします。
ストリーム演算子のオーバーロード
最もよくオーバーロードされる演算子は、断然、出力用の << です。これを自分のクラスのメンバにすることは できません。左オペランドが自分の型ではなく std::ostream(cout など)であり、ostream は自分の所有物ではないからです。そのため、これは常に、ストリームを非 const 参照で受け取って返す非メンバ関数になります。
これを成り立たせている要点は2つです。ストリームは 参照 (ostream&) で渡され、返されます。ストリームはコピーできず、同じストリームを返すことこそが cout << "p = " << p << "\n" の連鎖を可能にします。各 << がストリームを返すので、次の << が結び付く相手を得られるのです。return os; を忘れると連鎖は壊れます。
比較演算子
オブジェクトを ==、< などで比較するには、比較演算子をオーバーロードします。C++20 より前は一つひとつ手で書いていました。重要な落とし穴は、operator< が bool を返し、一貫した 順序を定義しなければならないことです。
6つの比較(==、!=、<、<=、>、>=)をすべて手で書くのは面倒でミスも起きやすいものです。C++20 では 三方比較 演算子 <=>(「宇宙船」演算子)が追加されました。これと == を default にすれば、すべての比較が自動生成されます。
= default はコンパイラに、メンバを宣言順に比較するよう指示します。これはまさに、自分で手書きする辞書順の比較そのものです。モダンなコンパイラではこちらを優先しましょう。
代入演算子とその罠
operator=(コピー代入)は特別です。コンパイラが自動生成してくれますし、単純なクラスならその既定の動作で正しく動きます。自前で書く必要があるのは、クラスがリソース(生のメモリ、ファイルハンドルなど)を管理していて、メンバごとのコピーでは正しくない場合だけです。標準的なシグネチャは、代入を連鎖できる(a = b = c)ように *this を参照で返します。
この短い関数には2つの罠があります。1つ目は 自己代入チェック if (this == &other) です。これがないと a = a のときに delete[] data してから、解放されたばかりの other.data を読み込むことになり、未定義動作になります。2つ目は順序が重要だということです。手書き版では、新しいバッファを安全にコピーし終える前に古いバッファを削除してはいけません(実際の実装では、まず確保してから、あるいは copy-and-swap イディオムを使って、確保に失敗してもオブジェクトが無傷のままになるようにすることが多いです)。
より広い落とし穴: 驚くような形で演算子をオーバーロードしないでください。左オペランドをこっそり変更する operator+ や、対称でない operator== は、読む人すべてを混乱させ、通常の意味を前提とする標準ライブラリのコードを壊します。演算子のオーバーロードは、その演算が自分の型にとって本当に「加算のようなもの」や「等価のようなもの」であるときだけにしましょう。
次へ: アクセス指定子
どの例でもデータメンバを private に保ち、コンストラクタ、演算子、いくつかのメソッドという小さな公開面を通じて振る舞いを公開していたことに注目してください。外の世界から見えるものと、クラスの内側に隠されているものとの間にあるこの境界を制御するのが アクセス指定子 です。public、private、protected がそれです。次は、それぞれが具体的に何を許すのか、なぜ private データと公開メソッドの組み合わせが良いカプセル化の既定なのか、そして protected が継承にどう関わるのかを見ていきます。
よくある質問
C++の演算子オーバーロードとは何ですか?
演算子オーバーロードを使うと、+、==、<< のような組み込み演算子が 自作の型 に対して何を意味するかを定義できます。operator+、operator== などの特別な名前の関数を書くと、その演算子が自分のクラスのオペランドとともに現れたときにコンパイラがそれを呼び出します。string + string が連結を行い、cout << obj が独自オブジェクトを出力するのは、この仕組みによるものです。
C++では演算子はメンバ関数にすべきですか、それとも非メンバ(friend)関数にすべきですか?
左オペランドが自分のクラスで、変換が不要な場合(例: +=、[]、())は メンバ関数 を使います。左オペランドが組み込み型になりうる場合や、両側で対称的な変換が必要な場合は 非メンバ 関数(多くは friend)を使います。operator<< ではこれが必須です。左オペランドが自分のクラスではなく std::ostream だからです。
オーバーロードできないC++の演算子はどれですか?
::(スコープ解決)、.(メンバアクセス)、.*(メンバポインタアクセス)、?:(三項)、sizeof はオーバーロードできません。また、まったく新しい演算子を作り出したり、演算子の項数(オペランドの数)や優先順位を変えたりすることもできません。+ は int を足すときでも自作の Vector2 を足すときでも、常に同じ優先順位の二項演算子です。