オペレータオーバーロード

+, -, ==, <<などを自分のクラスで定義。自然な記法。

オペレータオーバーロードとは

オペレータオーバーロードは、 既存の演算子を自分のクラスに対して再定義することです。

これにより、カスタムクラスでも標準型のような自然な記法を使えます。

比較演算子のオーバーロード

==!= などの比較演算子を定義します。

#include <iostream>
#include <string>

class Person {
private:
    std::string name;
    int age;
    
public:
    Person(const std::string& n, int a) : name(n), age(a) {}
    
    // operator== のオーバーロード
    bool operator==(const Person& other) const {
        return name == other.name && age == other.age;
    }
    
    // operator!= のオーバーロード
    bool operator!=(const Person& other) const {
        return !(*this == other);  // operator== を利用
    }
    
    // operator< のオーバーロード(ソート用)
    bool operator<(const Person& other) const {
        if (age != other.age) return age < other.age;
        return name < other.name;
    }
    
    void print() const {
        std::cout << name << " (" << age << "歳)" << std::endl;
    }
};

int main() {
    Person p1("太郎", 25);
    Person p2("太郎", 25);
    Person p3("花子", 23);
    
    if (p1 == p2) {
        std::cout << "p1 と p2 は同じ人" << std::endl;
    }
    
    if (p1 != p3) {
        std::cout << "p1 と p3 は異なる人" << std::endl;
    }
    
    if (p3 < p1) {
        std::cout << "p3 は p1 より年下" << std::endl;
    }
    
    return 0;
}

p1 と p2 は同じ人
p1 と p3 は異なる人
p3 は p1 より年下

算術演算子のオーバーロード

+, -, *, / などの算術演算子も定義できます。

#include <iostream>
#include <cmath>

class Vector2D {
private:
    double x, y;
    
public:
    Vector2D(double x_ = 0, double y_ = 0) : x(x_), y(y_) {}
    
    // operator+ のオーバーロード
    Vector2D operator+(const Vector2D& other) const {
        return Vector2D(x + other.x, y + other.y);
    }
    
    // operator- のオーバーロード
    Vector2D operator-(const Vector2D& other) const {
        return Vector2D(x - other.x, y - other.y);
    }
    
    // operator* (スカラー倍)
    Vector2D operator*(double scalar) const {
        return Vector2D(x * scalar, y * scalar);
    }
    
    // スカラー倍の逆順 (scalar * vector) 用
    friend Vector2D operator*(double scalar, const Vector2D& v) {
        return v * scalar;
    }
    
    // 内積
    double operator*(const Vector2D& other) const {
        return x * other.x + y * other.y;
    }
    
    // ノルム(大きさ)
    double norm() const {
        return std::sqrt(x * x + y * y);
    }
    
    void print() const {
        std::cout << "(" << x << ", " << y << ")" << std::endl;
    }
};

int main() {
    Vector2D v1(3, 4);
    Vector2D v2(1, 2);
    
    Vector2D sum = v1 + v2;
    std::cout << "v1 + v2 = ";
    sum.print();
    
    Vector2D diff = v1 - v2;
    std::cout << "v1 - v2 = ";
    diff.print();
    
    Vector2D scaled = v1 * 2;
    std::cout << "v1 * 2 = ";
    scaled.print();
    
    double dotProduct = v1 * v2;
    std::cout << "v1・v2 (内積) = " << dotProduct << std::endl;
    
    std::cout << "|v1| (ノルム) = " << v1.norm() << std::endl;
    
    return 0;
}

v1 + v2 = (4, 6)
v1 - v2 = (2, 2)
v1 * 2 = (6, 8)
v1・v2 (内積) = 11
|v1| (ノルム) = 5

代入演算子のオーバーロード

operator= は代入を制御します。 自己代入チェックと異なる演算子(+=など)の実装も重要です。

#include <iostream>

class Counter {
private:
    int value;
    
public:
    Counter(int v = 0) : value(v) {}
    
    // operator= (代入演算子)
    Counter& operator=(const Counter& other) {
        if (this != &other) {
            value = other.value;
        }
        std::cout << "operator= 呼び出し" << std::endl;
        return *this;
    }
    
    // operator+= (加算代入)
    Counter& operator+=(const Counter& other) {
        value += other.value;
        return *this;
    }
    
    // operator++ (前置インクリメント)
    Counter& operator++() {
        ++value;
        return *this;
    }
    
    // operator++ (後置インクリメント)
    Counter operator++(int) {  // int パラメータで区別
        Counter temp(value);
        ++value;
        return temp;
    }
    
    int getValue() const { return value; }
    
    void print() const {
        std::cout << "value = " << value << std::endl;
    }
};

int main() {
    Counter c1(5);
    Counter c2(3);
    
    c1 = c2;     // operator= 呼び出し
    c1.print();
    
    c1 += c2;    // operator+=
    c1.print();
    
    Counter c3 = c1;  // operator= 呼び出し
    c3.print();
    
    ++c1;        // 前置インクリメント
    c1.print();
    
    Counter c4 = c1++;  // 後置インクリメント(一時オブジェクト返却)
    std::cout << "c4 = " << c4.getValue() << ", c1 = " << c1.getValue() << std::endl;
    
    return 0;
}

operator= 呼び出し
value = 3
value = 6
operator= 呼び出し
value = 6
value = 7
c4 = 7, c1 = 8

ストリーム出力のオーバーロード(<<)

operator<< をオーバーロードで、 カスタム型を cout で出力できます。

通常、friend 関数として実装されます。

#include <iostream>
#include <string>

class Student {
private:
    std::string name;
    int grade;
    double gpa;
    
public:
    Student(const std::string& n, int g, double gp)
        : name(n), grade(g), gpa(gp) {}
    
    // friend 関数として operator<< を宣言
    friend std::ostream& operator<<(std::ostream& os, const Student& s);
};

// friend 関数の定義
std::ostream& operator<<(std::ostream& os, const Student& s) {
    os << "生徒: " << s.name
       << " | 学年: " << s.grade
       << " | GPA: " << s.gpa;
    return os;
}

int main() {
    Student s1("太郎", 2, 3.8);
    Student s2("花子", 1, 3.9);
    
    std::cout << s1 << std::endl;
    std::cout << s2 << std::endl;
    
    return 0;
}

生徒: 太郎 | 学年: 2 | GPA: 3.8
生徒: 花子 | 学年: 1 | GPA: 3.9

配列アクセス演算子のオーバーロード([])

operator[] で配列のような直感的アクセスを実現します。

#include <iostream>
#include <vector>
#include <stdexcept>

class SimpleArray {
private:
    std::vector<int> data;
    
public:
    SimpleArray(int size) : data(size, 0) {}
    
    // operator[] (範囲チェック付き)
    int& operator[](int index) {
        if (index < 0 || index >= static_cast<int>(data.size())) {
            throw std::out_of_range("インデックス外");
        }
        return data[index];
    }
    
    // const 版
    int operator[](int index) const {
        if (index < 0 || index >= static_cast<int>(data.size())) {
            throw std::out_of_range("インデックス外");
        }
        return data[index];
    }
    
    int size() const { return data.size(); }
};

int main() {
    SimpleArray arr(5);
    
    // 書き込み
    for (int i = 0; i < arr.size(); ++i) {
        arr[i] = i * 10;
    }
    
    // 読み取り
    std::cout << "配列内容: ";
    for (int i = 0; i < arr.size(); ++i) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
    
    // 範囲外アクセス
    try {
        arr[10] = 99;
    } catch (const std::out_of_range& e) {
        std::cout << "エラー: " << e.what() << std::endl;
    }
    
    return 0;
}

配列内容: 0 10 20 30 40
エラー: インデックス外

型変換演算子のオーバーロード

operator T() で暗黙的な型変換を定義します。 C++11 では explicit を使って安全性を高められます。

#include <iostream>
#include <string>

class Fraction {
private:
    int numerator;    // 分子
    int denominator;  // 分母
    
public:
    Fraction(int n, int d = 1) 
        : numerator(n), denominator(d) {}
    
    // double への暗黙的変換
    operator double() const {
        return static_cast<double>(numerator) / denominator;
    }
    
    // 文字列への explicit 変換
    explicit operator std::string() const {
        return std::to_string(numerator) + "/" + std::to_string(denominator);
    }
};

int main() {
    Fraction f(3, 4);
    
    // 暗黙的に double に変換
    double d = f;
    std::cout << "Fraction(3, 4) = " << d << std::endl;
    
    // 暗黙的変換を使用した計算
    double result = f + 0.5;
    std::cout << "Fraction(3, 4) + 0.5 = " << result << std::endl;
    
    // explicit 変換は明示的に呼び出す
    std::string str = static_cast<std::string>(f);
    std::cout << "文字列: " << str << std::endl;
    
    return 0;
}

Fraction(3, 4) = 0.75
Fraction(3, 4) + 0.5 = 1.25
文字列: 3/4

よくある誤り

誤り1: operator= で *this を返さない

class Bad {
public:
    void operator=(const Bad& other) {  // void を返す
        // ...
    }
};

int main() {
    Bad b1, b2, b3;
    // b1 = b2 = b3;  // エラー! チェーン不可
}

誤り2: 後置インクリメントで int パラメータなし

class Bad {
public:
    Bad operator++() {  // 前置と同じシグニチャ
        // ...
    }
};

誤り3: operator<< をメンバ関数с

class Bad {
public:
    // メンバ関数だと左オペランドが *this になる
    std::ostream& operator<<(std::ostream& os) {
        return os << "...";
    }
};

int main() {
    Bad b;
    // std::cout << b;  // (*b) << std::cout の順になってしまう
    // b << std::cout;  // こうなる
}

オーバーロード不可能な演算子

以下の演算子はオーバーロードできません:

  • :: (スコープ解決)
  • . (メンバアクセス)
  • .* (メンバポインタアクセス)
  • ?: (三項条件演算子)
  • # (プリプロセッサ)

ポイント

  • 比較演算子:==, !=, <, > などを定義
  • 算術演算子:+, -, *, / などを定義
  • 代入演算子:operator= は *this を返す
  • 前置と後置:operator++() と operator++(int)
  • ストリーム演算子:<< は friend 関数
  • 配列アクセス:operator[] で [] 記法をサポート
  • 型変換:explicit で安全性向上(C++11)

やってみよう

練習1: 複素数クラスで +, -, ==, << をオーバーロード

練習2: Rational (有理数) クラスで算術演算を実装

練習3: String クラスで +(連結)と [](アクセス)を実装

チャレンジ: Matrix クラスで * (行列積)をオーバーロード

まとめ

  • オペレータオーバーロードで自然な記法が実現できる
  • メンバ関数と friend 関数の使い分けが重要
  • operator= は *this を返すこと、前後置の区別、自己代入チェック必須
  • 過度なオーバーロードは可読性低下につながる