C++ Learning

第27回 演算子オーバーロード

+, ==, << などの演算子を、自作クラスに対しても動作させるのが演算子オーバーロードです。std::string s1 + s2std::cout << xv1 == v2 ― すべてこの仕組み。「演算子 = 関数名が記号」だと気づけば怖くありません。ただし濫用するとコードが読めなくなるので、使いどころも学びます。

このページで押さえること
✅ 最低限ここだけ覚える
  • 演算子は関数名が特殊な関数operator+ など)
  • 自作クラスに +== を定義できる
  • メンバ関数版と非メンバ(フリー関数)版がある
  • 「意味が自然でない」演算子の追加は避ける
⭐ 余裕があれば読む
  • operator<< は必ず非メンバで書く
  • 前置/後置インクリメント区別
  • C++20 の operator<=>(宇宙船演算子)
  • 変換演算子 operator bool()

1. まず触ってみる ― Vector2D

2D ベクトルの足し算を + で書けるようにしてみましょう。

▶ Vector2D の加算
v1 = (, )
v2 = (, )

最小コード

first_op.cpp最小例
struct Vec2 { double x, y; // + 演算子をメンバ関数として定義 Vec2 operator+(const Vec2& other) const { return {x + other.x, y + other.y}; } }; Vec2 a{1, 2}, b{3, 4}; Vec2 c = a + b; // (4, 6) ← a.operator+(b) と同じ
ここまでで覚えること(2 つ):
  • 演算子の正体は operator+ という関数名が記号の関数
  • a + ba.operator+(b) の糖衣構文

よくある素朴な疑問

Q. なんでもかんでも + にできるの?
→ 技術的には可能ですが、意味が自然な場合だけにするのが鉄則。数学的なベクトル・行列・複素数、あるいは文字列連結、のような自然な解釈がある演算子だけ。Dog + Cat のように意味不明な組み合わせは避けます。

Q. オペレータの優先順位は変えられる?
変えられません。 +* の優先順位は組み込み型と同じ。

Q. 新しい演算子(例:** で冪乗)は作れる?
作れません。 既存の演算子だけ再定義できます。新しい記号は power(a, b) のような普通の関数にします。

2. 演算子は関数名が記号なだけ

C++ の演算子はすべて関数呼び出しに展開されます。

書いた式実際に呼ばれる関数
a + ba.operator+(b) または operator+(a, b)
a == ba.operator==(b) または operator==(a, b)
a < ba.operator<(b) または operator<(a, b)
++a (前置)a.operator++()
a++ (後置)a.operator++(int)(int はダミー)
std::cout << aoperator<<(std::cout, a)
a[i]a.operator[](i)
a()a.operator()()

つまり演算子オーバーロードとは「これらの特殊な名前の関数を自作クラスに定義する」ことにすぎません。

3. メンバ関数 vs 非メンバ

演算子はメンバ関数としても非メンバ(フリー関数)としても書けます。

メンバ版this が左辺
struct Vec2 { double x, y; Vec2 operator+(const Vec2& rhs) const { return {x + rhs.x, y + rhs.y}; } };
非メンバ版両辺平等
struct Vec2 { double x, y; }; Vec2 operator+(const Vec2& a, const Vec2& b) { return {a.x + b.x, a.y + b.y}; }

どちらを選ぶ?

原則:「対称」な演算子(+, -, ==)は非メンバ「自分を変える」演算子(+=, ++)はメンバにするのが Scott Meyers の推奨:

一般的なパターン:+= を基礎に + を作る

Vec2 完全版推奨パターン
struct Vec2 { double x, y; // メンバ: += (自身を書き換える) Vec2& operator+=(const Vec2& rhs) { x += rhs.x; y += rhs.y; return *this; // メソッドチェーン可 } }; // 非メンバ: + は += を使って実装 Vec2 operator+(Vec2 a, const Vec2& b) { a += b; // 左辺のコピーに += を適用 return a; }

こうすると「+= のロジックは 1 箇所」にまとまり、a + ba += b の挙動が必ず一致します。

4. 比較演算子と出力演算子

比較演算子

STL コンテナ(std::sort, std::set, std::map)で使うためには <== がよく必要になります。

比較演算子STL 連携
struct Point { int x, y; }; // == と != はペアで定義するのが慣習 bool operator==(const Point& a, const Point& b) { return a.x == b.x && a.y == b.y; } bool operator!=(const Point& a, const Point& b) { return !(a == b); } // 辞書順比較 bool operator<(const Point& a, const Point& b) { if (a.x != b.x) return a.x < b.x; return a.y < b.y; } std::set<Point> s; // operator< があるから使える
C++20 の宇宙船演算子: operator<=> を 1 つ定義すると、6 個の比較演算子(<, <=, >, >=, ==, !=)が自動生成されます。ただし本サイトは C++17 基準なので、従来の書き方で進めます。

operator<< ― 出力演算子

std::cout << p; と書けるようにする。非メンバで書くのが必須(左辺が std::ostream で変更できないため)。

operator<<非メンバ
#include <iostream> std::ostream& operator<<(std::ostream& os, const Point& p) { os << '(' << p.x << ", " << p.y << ')'; return os; // チェーンを可能にする } Point p{3, 4}; std::cout << p << "\n"; // (3, 4)

ポイントは 3 つ:

ここまでで演算子オーバーロードの基本は OK
最後の §5 は設計原則。濫用を避けるための思想。

5. 使いどころと設計原則

やるべきケース

やるべきでないケース

最小驚きの法則(Principle of Least Astonishment): 演算子の意味は数学や既存の慣習に素直に従うこと。
  • a + b == b + a が直感的に成り立つか?(可換性)
  • a == b!(a != b) が同じ?(整合性)
  • a < bb > a が同じ?
これらが崩れる設計は、利用者に地雷を埋め込むことになります。

オーバーロードしてはいけない演算子

次の演算子は絶対にオーバーロードできない(言語規格で禁止):

まとめ: 演算子オーバーロードは強力だが諸刃の剣。「この演算子を自分のクラスに定義すれば、STL や標準入出力と自然に連携できるか?」を判断基準にすれば、ほぼ間違いはありません。
広告スペース

確認クイズ

演算子オーバーロードを 4 問で確認。

Q1. a + b という式が実際に呼び出すのは?

add(a, b)
a.add(b)
a.operator+(b) または operator+(a, b)
plus(a, b)
演算子は関数名が記号の関数+ なら operator+。メンバ関数として定義されていれば a.operator+(b)、非メンバなら operator+(a, b) が呼ばれます。

Q2. operator<<std::cout << obj)は、メンバ関数・非メンバ関数のどちらで書くべき?

メンバ関数
非メンバ関数
両方同時に
どちらでも同じ
operator<< は左辺が std::ostream(こちらは変更できない)なので、メンバ関数としては追加できません。必ず非メンバで書く必要があります。

Q3. 新しい演算子 **(累乗)を自作クラスに追加したい。可能?

operator** を定義すれば可能
不可能(既存の演算子しかオーバーロードできない)
#define で可能
C++20 からなら可能
C++ では既存の演算子のみオーバーロードできます。新しい記号は作れません。累乗は pow(a, b)a.pow(b) のような普通の関数にします。

Q4. a + ba += b; に対して演算子を定義するとき、推奨される実装順は?

+ を先に実装して += はそれを呼び出す
+= を先に実装して + はそれを使う
どちらも独立に別ロジックで書く
+= だけ定義すれば + は自動生成される
+=(自身を変更)を基礎にして + を実装するのがモダン C++ の定石:
Vec operator+(Vec a, const Vec& b) { a += b; return a; }
これで ++= の挙動が必ず一致し、コードの重複もなくなります。