関数オーバーロードと演算子オーバーロード¶
一言で言うと¶
- 関数オーバーロード: 同名の関数を引数の型・数違いで複数定義し、コンパイル時に引数から選ばせる仕組み
- 演算子オーバーロード:
+==[]などの演算子を自作型に定義する仕組み。数学的な型(ベクトル、行列)を自然に書くための道具
どちらも「呼ばれる関数がコンパイル時に決まる」点で、実行時に決まる仮想関数と対照的です(アドホック多態 → ポリモーフィズム)。
関数オーバーロード¶
// C++20
void Spawn(EnemyId id); // IDで
void Spawn(EnemyId id, Vec2 pos); // 位置指定で
void Spawn(const EnemyDesc& desc); // 詳細指定で
// 呼び出し: Spawn(id) — 引数の型から最適な候補をコンパイラが選ぶ(オーバーロード解決)
落とし穴: 暗黙変換との相互作用¶
void Play(int soundId);
void Play(float volume);
Play(0.5); // doubleリテラル → intにもfloatにも変換可能 → 曖昧エラー(まだ良い)
Play('a'); // charはintに昇格 → Play(int)が呼ばれる(意図か?)
オーバーロード解決の規則は複雑(完全一致 > 昇格 > 変換)で、「似た型の引数を持つオーバーロード」は事故の温床。意味が違うなら名前を分ける(PlayById/PlayWithVolume)か、強い型(struct SoundId { int v; };)を使う方が安全です。
演算子オーバーロード¶
// C++20 — ゲーム数学の定番
struct Vec2 {
float x = 0, y = 0;
Vec2 operator+(const Vec2& o) const { return {x + o.x, y + o.y}; }
Vec2 operator*(float s) const { return {x * s, y * s}; }
Vec2& operator+=(const Vec2& o) { x += o.x; y += o.y; return *this; }
bool operator==(const Vec2& o) const = default; // C++20: 比較の自動生成
};
Vec2 operator*(float s, const Vec2& v) { return v * s; } // 2.0f * v も書けるように(非メンバ)
// 使用: pos += velocity * dt; ← 演算子なしだと pos = Add(pos, Mul(velocity, dt));
検証済みサンプル: samples/operator_overload.cpp
C++20の進歩: <=> と = default¶
struct Version {
int major = 0, minor = 0;
auto operator<=>(const Version&) const = default; // ==, <, >, <=, >= が全部手に入る
};
設計ルール(乱用防止)¶
- 意味が数学的に自明な場合だけ使う(
+が加算以外の意味なら関数名で書く)。Player + Itemのような「クリエイティブな」演算子は読み手への攻撃 ==を定義するなら一貫した同値関係に(!=はC++20なら自動導出)- 算術演算子は値を返し、複合代入(
+=)は参照を返す、という慣習に従う - 変換演算子(
operator bool等)はexplicitを付ける(暗黙変換事故の防止)
メモリ・コンパイル上で起きること¶
- オーバーロードされた関数はそれぞれ別の関数としてコンパイルされ、名前修飾(mangling)で区別される(
Spawn(int)とSpawn(float)は別シンボル)。C言語にオーバーロードがないのはこの仕組みがないため - 演算子も普通の関数呼び出しにコンパイルされる(
a + bはa.operator+(b))。小さい演算子はインライン化され、手書きのAdd関数と機械語は同じ——「演算子は遅い」は誤解
C#との違い¶
- C#にもオーバーロードと演算子オーバーロード(
public static Vector2 operator+)があり、概念は同じ - 違い: C#の演算子は必ずstatic、C++はメンバ/非メンバを選べる。C++は
[]()->*など多くの演算子を定義できる(スマートポインタは->のオーバーロードでできている) - C++にはユーザー定義リテラル(
10_secのような接尾辞)まである(使いすぎ注意)
Unity開発者が誤解しやすい点¶
- UnityのVector3の
+*を「エンジンの特別機能」と思っている——ただの演算子オーバーロードで、C++でも自作できる(しかもインライン化でコスト0) ==の罠: UnityEngine.Objectの==は「破棄済みならnull扱い」という特殊オーバーロード。C++で自作型の==を書くときも「何をもって等しいか」は設計判断(参照同一性か値の等価か)- オーバーロード解決は静的(変数の宣言型で決まる)。実行時の型では選ばれない(それは仮想関数の仕事)——
Base& b = derived; f(b);はf(Base&)を呼ぶ
ゲーム開発での使用例¶
- 数学型(Vec2/3/4, Quat, Matrix, Color): 演算子の主戦場
- 単位の強い型:
Seconds operator""_s(long double)等でミリ秒/秒の取り違えをコンパイルエラーに operator[]: グリッドやインベントリのインデックスアクセス- 関数オブジェクトの
operator(): ラムダの裏側
使う場面 / 使わない場面¶
- 使う: 数学的な型、コンテナ的な型、比較(C++20の
=defaultで無料) - 使わない: 意味が自明でない演算(
<<をログ以外に使う等)、副作用の大きい処理を演算子に隠す、暗黙変換演算子(explicitなしのoperator T)
よくある誤解¶
- 「オーバーロードとオーバーライドは似たもの」— 別物。オーバーロード=同名別引数(コンパイル時)、オーバーライド=仮想関数の上書き(実行時)
- 「演算子オーバーロードは遅い」— インライン化されれば手書き関数と同一
関連項目¶
- ポリモーフィズム(多態の分類)
- 名前修飾とシンボル
- テンプレート(オーバーロード解決とテンプレートの相互作用)
理解度チェック¶
- オーバーロードとオーバーライドの決定タイミングの違いは?
- 「似た型の引数のオーバーロード」が危険な理由は?
- Vec2の
+を演算子にする正当性と、Player + Itemが駄目な理由を、同じ基準で説明してください。
演習¶
samples/operator_overload.cpp のVec2に operator-、Dot(内積)を追加してください。内積を operator* にすべきか関数にすべきか、スカラー倍との衝突の観点から判断を書いてください。
前: 継承・仮想関数 | カテゴリ目次 | 次: テンプレートと concepts