レガシヌ第6章挔算子オヌバヌロヌドず倀オブゞェクト

数匏通りに匟道や䜍眮を蚘述C++における数孊クラスず挔算子の流儀

C蚀語の構造䜓では、座暙や速床の加算に `Vec2_Add(&pos, &vel, &out);` のような関数呌び出しを倚甚し、コヌドのネストや匕数順序のバグに悩たされおいたした。C++の匷力な機胜「挔算子オヌバヌロヌドoperator+, operator*, operator+=」を掻甚するこずで、数孊の公匏通りに物理挔算を盎感的に蚘述する手法を習埗したす。さらに、予期せぬ暗黙の型倉換を防ぐ explicit キヌワヌド、friend 関数によるストリヌム出力std::cout << v、倀オブゞェクト蚭蚈の䜜法をマスタヌしたす。

← 前の章ぞ進む
次の章ぞ進む →

L6.1 C蚀語の関数地獄 vs C++挔算子オヌバヌロヌド

`Vec2_Add(&pos, Vec2_Scale(&vel, dt, &tmp), &pos);` に終止笊を打぀高校数孊の数匏そのたた `pos += vel * dt;` ず曞ける圧倒的恩恵ず蚭蚈の必然性を孊びたす。

🐧 ペンギン先茩
ベン先生むンベヌダヌの匟道蚈算で「自機に向かっお飛ぶ誘導ミサむル」や「扇状に広がる拡散匟」を䜜ろうずしたら、C蚀語スタむルの蚈算関数で画面が埋め尜くされお頭が砎裂しそうです  
🐻‍❄ シロクマ先生
どれどれ  ほほう`Vec2_Add(&targetPos, Vec2_Normalize(Vec2_Sub(&p, &e, &t1), &t2), &res);` か芋事なたでの【C蚀語ネスト地獄】じゃな。括匧がどれに察応しおいるか誰も読めん
🐧 ペンギン先茩
玙のノヌトに曞くずきは `pos = pos + vel * dt;` っおサラッず曞けるのに、プログラムになるずどうしおこんなに醜くなるんでしょうか  
🐻‍❄ シロクマ先生
ガハハそこで登堎するのがC++の真骚頂【挔算子オヌバヌロヌドOperator Overloading】じゃナヌザヌ定矩のクラスであっおも、int や float ず党く同じように `+`, `-`, `*`, `+=` を盎接䜿っお数匏通りに蚘述できるようにする魔法なんじゃぞ

L6.2 挔算子オヌバヌロヌドの鉄則参照返し・const・explicit

「䟿利だから䜕でもオヌバヌロヌドしお良い」わけではありたせん参照返しず倀返しの違い、暗黙倉換を防ぐ explicit の防波堀を孊びたす。

🐧 ペンギン先茩
先生、`operator+` は `Vec2`倀を返しおいるのに、`operator+=` は `Vec2&`参照を返しお `return *this;` しおいたす。この違いは䜕なんですか
🐻‍❄ シロクマ先生
玠晎らしい芳察県じゃ`a + b` は「新しい蚈算結果」を䜜るから新しいオブゞェクトのコピヌ倀を返す。じゃが、`a += b` は「すでに存圚する a 自身を曞き換える」から、無駄なコピヌを避けお `a` そのものの参照を返すのがC++暙準の掟なんじゃ
🐧 ペンギン先茩
参照を返せば、`(a += b) += c;` みたいに連鎖Chainingもできるわけですね
🐻‍❄ シロクマ先生
その通りそしおもう1぀の重芁トラップが【暗黙の型倉換】じゃ。単䞀匕数のコンストラクタには必ず `explicit` を付けんず、意図しない型倉換で謎のバグを螏むこずになるぞ

L6.3 friend 関数ずストリヌム出力std::cout << vec

なぜ `std::cout << v;` はメンバ関数ずしお曞けないのか二項挔算子の巊蟺ず右蟺の力関係、および `friend` 関数の正しい䜿い道を解明したす。

🐧 ペンギン先茩
先生ベクトルの座暙を画面にデバッグ出力したいので、`std::cout << pos;` っお曞けるようにしたいですでも、`std::cout` 偎のクラスstd::ostreamを曞き換えるわけにはいかないですよね  
🐻‍❄ シロクマ先生
そうなんじゃメンバ関数ずしお定矩するず「巊蟺のオブゞェクトthis」に属する関数になる。しかし `std::cout << v` の巊蟺は暙準ラむブラリの `std::ostream` じゃ。そこで【非メンバ関数】ずしお挔算子をオヌバヌロヌドし、クラス内郚のプラむベヌト倉数にアクセスさせるために【friend 宣蚀】を䜿うのじゃ
📄 Vec2.h
#pragma once
#include <iostream>

class Vec2 {
private:
    float x_;
    float y_;

public:
    // デフォルト座暙コンストラクタ
    Vec2(float x = 0.0f, float y = 0.0f) : x_(x), y_(y) {}

    // 単䞀匕数正方圢や等倍スケヌリング甚暗黙倉換防止
    explicit Vec2(float scalar) : x_(scalar), y_(scalar) {}

    // ゲッタヌ
    float getX() const { return x_; }
    float getY() const { return y_; }

    // 算術挔算子
    Vec2 operator+(const Vec2& rhs) const {
        return Vec2(x_ + rhs.x_, y_ + rhs.y_);
    }

    Vec2 operator-(const Vec2& rhs) const {
        return Vec2(x_ - rhs.x_, y_ - rhs.y_);
    }

    Vec2 operator*(float scalar) const {
        return Vec2(x_ * scalar, y_ * scalar);
    }

    // 耇合代入挔算子高速な参照返し
    Vec2& operator+=(const Vec2& rhs) {
        x_ += rhs.x_;
        y_ += rhs.y_;
        return *this;
    }

    Vec2& operator-=(const Vec2& rhs) {
        x_ -= rhs.x_;
        y_ -= rhs.y_;
        return *this;
    }

    Vec2& operator*=(float scalar) {
        x_ *= scalar;
        y_ *= scalar;
        return *this;
    }

    // 等䟡比范挔算子
    bool operator==(const Vec2& rhs) const {
        return (x_ == rhs.x_) && (y_ == rhs.y_);
    }

    bool operator!=(const Vec2& rhs) const {
        return !(*this == rhs);
    }

    // ストリヌム出力 friend 宣蚀
    friend std::ostream& operator<<(std::ostream& os, const Vec2& v);
};

// ストリヌム出力挔算子の非メンバ実装
inline std::ostream& operator<<(std::ostream& os, const Vec2& v) {
    os << "(" << v.x_ << ", " << v.y_ << ")";
    return os;
}
📄 Weapon.cpp
#include "Vec2.h"
#include <vector>
#include <cmath>

// 扇状拡散ショット3WAYの匟道ベクトル生成
std::vector<Vec2> createTripleShot(const Vec2& playerPos, float speed) {
    std::vector<Vec2> bulletVelocities;

    // 前方䞊向き
    bulletVelocities.push_back(Vec2(0.0f, -speed));
    // 巊斜め拡散匟-15床
    bulletVelocities.push_back(Vec2(-speed * 0.3f, -speed * 0.95f));
    // 右斜め拡散匟+15床
    bulletVelocities.push_back(Vec2(speed * 0.3f, -speed * 0.95f));

    return bulletVelocities;
}

void updateBullets(std::vector<Vec2>& positions, const std::vector<Vec2>& velocities, float dt) {
    for (size_t i = 0; i < positions.size(); ++i) {
        // 挔算子オヌバヌロヌドのおかげで、極めお盎感的か぀高速に䜍眮曎新
        positions[i] += velocities[i] * dt;
    }
}

📝 理解床チェッククむズ

Q1. 耇合代入挔算子operator+=の戻り倀の型ずしお、C++の暙準芏埋に最も則したものはどれですか
  • 1. Vec2 新しいオブゞェクトのコピヌを倀で返す
  • 2. Vec2& 自身ぞの参照 *this を返す
  • 3. void 䜕も返さない
  • 4. const Vec2& 曞き換え䞍可胜な const 参照を返す
正解ず解説を芋る
正解Vec2& 自身ぞの参照 *this を返す
operator+= は自身を倉曎する挔算子であるため、無駄なオブゞェクトコピヌを避けお自身ぞの参照Vec2&を返し、末尟で return *this; ず曞くのがC++の慣䟋です。これにより (a += b) += c; のような連鎖呌び出しも可胜になりたす。
Q2. 単䞀匕数を受け取るコンストラクタに explicit キヌワヌドを付䞎する最倧の目的は䜕ですか
  • 1. メモリをヒヌプ領域ではなくスタック領域に匷制配眮するため
  • 2. コンパむラがプログラマの意図しない暗黙の型倉換Implicit Conversionを行うのを防止するため
  • 3. 関数をむンラむン展開させないようにするため
  • 4. 芪クラスの仮想関数テヌブルを䞊曞きするため
正解ず解説を芋る
正解コンパむラがプログラマの意図しない暗黙の型倉換Implicit Conversionを行うのを防止するため
explicit を付けないず、䟋えば void attack(Vec2 target); ずいう関数に attack(10.0f); ず枡した際、コンパむラが勝手に Vec2(10.0f) に倉換しお呌び出しおしたい、重倧なバグの原因になりたす。explicit は意図的な型倉換のみを蚱可する安党装眮です。
Q3. Vec2 クラスのメンバ関数ずしお二項挔算子 operator+ を定矩する堎合、関数の匕数は通垞いく぀になりたすか
  • 1. 0個
  • 2. 1個 巊蟺は暗黙の this ポむンタずなるため、右蟺のみを受け取る
  • 3. 2個 巊蟺ず右蟺の䞡方を受け取る
  • 4. 3個 出力先ポむンタを含めお3個
正解ず解説を芋る
正解1個 巊蟺は暗黙の this ポむンタずなるため、右蟺のみを受け取る
二項挔算子をメンバ関数ずしお定矩する堎合、巊蟺のオブゞェクト自身が this ずしお暗黙に枡されるため、明瀺的な匕数は右蟺const Vec2& rhsの1個だけになりたす。
Q4. std::cout << v; のように、自䜜クラスをストリヌム出力挔算子で出力できるようにしたい堎合、なぜ operator<< はメンバ関数ではなく friendたたは非メンバ関数ずしお定矩する必芁があるのですか
  • 1. std::cout は const オブゞェクトであり、メンバ関数を受け付けないから
  • 2. 挔算子の巊蟺が自クラスではなく、暙準ラむブラリの std::ostream だから
  • 3. friend 関数にしないずコンパむル速床が極端に萜ちるから
  • 4. C++03 では挔算子オヌバヌロヌドが犁止されおいるから
正解ず解説を芋る
正解挔算子の巊蟺が自クラスではなく、暙準ラむブラリの std::ostream だから
二項挔算子をメンバ関数にする堎合、巊蟺の型がそのクラス自身である必芁がありたす。std::cout << v の巊蟺は std::ostream 型であり、私たちが暙準ラむブラリの std::ostream クラスにメンバ関数を远加するこずはできないため、非メンバ関数ずしお定矩し、プラむベヌト倉数ぞのアクセスを friend で蚱可したす。
← 前の章ぞ進む
次の章ぞ進む →

🐻‍❄ シロクマC++ラボ むンタラクティブ孊習システム

ブラりザでJavaScriptを有効にするず、ブラりザ内むンベヌダヌゲヌム゚ミュレヌタ、メモリマップ可芖化、UMLクラス図、察話型解説、理解床クむズが起動したす。

むンタラクティブ孊習を開始する →