第5章:演算子オーバーロードとコピーセマンティクス

はじめに

演算子オーバーロードは、ユーザー定義型に対して組み込み型と同様の自然な構文を提供する機能です。これにより、クラスのインターフェースがより直感的になります。

本章では、演算子オーバーロードの基本から、C++で最も重要なパターンの一つであるOrthodox Canonical Formについて学びます。

---

1. 演算子オーバーロードの基礎

1.1 なぜ演算子オーバーロードか

// 演算子オーバーロードなし
Complex c1(1, 2), c2(3, 4);
Complex c3 = c1.add(c2);  // 不自然

// 演算子オーバーロードあり
Complex c1(1, 2), c2(3, 4);
Complex c3 = c1 + c2;  // 自然!

Bjarne Stroustrupは次のように述べています:

> "Operator overloading is the ability to tell the compiler how to perform a certain mathematical or logical operation when its corresponding operator is used on one or more variables."

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

オーバーロード可能

+  -  *  /  %  ^  &  |  ~  !  =  <  >
+=  -=  *=  /=  %=  ^=  &=  |=
<<  >>  >>=  <<=  ==  !=  <=  >=
&&  ||  ++  --  ,  ->*  ->
()  []  new  delete  new[]  delete[]

オーバーロード不可

::   .   .*   ?:   sizeof   typeid

1.3 メンバ関数 vs 非メンバ関数

class Complex {
    double real, imag;
public:
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}

    // メンバ関数として定義
    Complex operator+(const Complex& other) const {
        return Complex(real + other.real, imag + other.imag);
    }

    // フレンド関数として定義
    friend Complex operator-(const Complex& a, const Complex& b);

    // アクセサ
    double getReal() const { return real; }
    double getImag() const { return imag; }
};

// 非メンバ関数として定義
Complex operator-(const Complex& a, const Complex& b) {
    return Complex(a.real - b.real, a.imag - b.imag);
}

使い分けの指針

  • メンバ関数: =, [], (), ->, 複合代入演算子
  • 非メンバ関数: 二項演算子(対称性のため)、ストリーム演算子

---

2. 代入演算子

2.1 コピー代入演算子

class String {
    char* data;
    size_t len;

public:
    // コピー代入演算子
    String& operator=(const String& other) {
        if (this != &other) {  // 自己代入チェック
            delete[] data;     // 古いリソースを解放

            len = other.len;
            data = new char[len + 1];
            std::strcpy(data, other.data);
        }
        return *this;
    }
};

2.2 自己代入チェック

自己代入チェックは重要です:

String s("Hello");
s = s;  // 自己代入

// 自己代入チェックがないと:
// 1. delete[] data; で data を解放
// 2. data = new char[...]; で新しい領域を確保
// 3. strcpy(data, other.data); で解放済みの領域からコピー → 未定義動作!

2.3 コピーアンドスワップイディオム

例外安全な代入演算子の実装パターン:

class String {
    char* data;
    size_t len;

    void swap(String& other) noexcept {
        std::swap(data, other.data);
        std::swap(len, other.len);
    }

public:
    // コピーコンストラクタ(別途実装)
    String(const String& other);

    // コピーアンドスワップイディオム
    String& operator=(String other) {  // 値渡し(コピーが作られる)
        swap(other);  // this と other を交換
        return *this;
    }  // other のデストラクタで古いリソースが解放される
};

利点:

  • 自己代入安全: 値渡しでコピーを作成
  • 例外安全: コピー失敗時も元のオブジェクトは無傷
  • 簡潔: 自己代入チェック不要
  • ---

    3. コピーコンストラクタ

    3.1 シャローコピー vs ディープコピー

    class ShallowCopy {
        int* data;
    public:
        ShallowCopy(int val) : data(new int(val)) {}
    
        // コンパイラ生成のコピーコンストラクタ(シャローコピー)
        // ShallowCopy(const ShallowCopy& other) : data(other.data) {}
    
        ~ShallowCopy() { delete data; }  // 二重解放の危険!
    };
    
    class DeepCopy {
        int* data;
    public:
        DeepCopy(int val) : data(new int(val)) {}
    
        // ディープコピー
        DeepCopy(const DeepCopy& other) : data(new int(*other.data)) {}
    
        ~DeepCopy() { delete data; }  // 安全
    };
    

    3.2 暗黙のコピーコンストラクタ

    コンパイラが自動生成するコピーコンストラクタは、メンバごとのコピー(シャローコピー)を行います:

    class Auto {
        int a;
        double b;
        std::string s;  // std::stringは独自のディープコピーを持つ
    public:
        // コンパイラが自動生成:
        // Auto(const Auto& other) : a(other.a), b(other.b), s(other.s) {}
    };
    

    3.3 コピーの禁止

    コピーを禁止したい場合:

    // C++11以降
    class NonCopyable {
    public:
        NonCopyable() = default;
        NonCopyable(const NonCopyable&) = delete;
        NonCopyable& operator=(const NonCopyable&) = delete;
    };
    
    // C++98
    class NonCopyable98 {
    private:
        NonCopyable98(const NonCopyable98&);  // 宣言のみ、定義なし
        NonCopyable98& operator=(const NonCopyable98&);
    public:
        NonCopyable98() {}
    };
    

    ---

    4. Orthodox Canonical Form

    4.1 定義

    42のCPP Modulesで重要視されるOrthodox Canonical Form(OCF)とは、クラスが以下の4つの特殊メンバ関数を明示的に定義することです:

  • デフォルトコンストラクタ
  • コピーコンストラクタ
  • コピー代入演算子
  • デストラクタ

4.2 完全な実装例

class Sample {
private:
    int* data;
    std::string name;

public:
    // 1. デフォルトコンストラクタ
    Sample() : data(new int(0)), name("default") {
        std::cout << "Default constructor" << std::endl;
    }

    // 2. コピーコンストラクタ
    Sample(const Sample& other) : data(new int(*other.data)), name(other.name) {
        std::cout << "Copy constructor" << std::endl;
    }

    // 3. コピー代入演算子
    Sample& operator=(const Sample& other) {
        std::cout << "Copy assignment operator" << std::endl;
        if (this != &other) {
            delete data;
            data = new int(*other.data);
            name = other.name;
        }
        return *this;
    }

    // 4. デストラクタ
    ~Sample() {
        std::cout << "Destructor" << std::endl;
        delete data;
    }

    // その他のメンバ
    void setData(int val) { *data = val; }
    int getData() const { return *data; }
};

4.3 Rule of Three

C++98での経験則:

> "If you need to explicitly declare the destructor, copy constructor, or copy assignment operator, you probably need to explicitly declare all three." > > (デストラクタ、コピーコンストラクタ、コピー代入演算子のいずれかを明示的に定義する必要がある場合、おそらく3つすべてを明示的に定義する必要がある)

理由:リソース管理が必要なクラスでは、すべての操作でリソースを正しく扱う必要があるため。

4.4 Rule of Five(C++11以降)

C++11では、ムーブセマンティクスが追加され、5つになりました:

  • デストラクタ
  • コピーコンストラクタ
  • コピー代入演算子
  • ムーブコンストラクタ
  • ムーブ代入演算子
  • 4.5 Rule of Zero

    モダンC++では、可能な限りリソース管理をスマートポインタに任せ、特殊メンバ関数を書かない「Rule of Zero」が推奨されます:

    class Modern {
        std::unique_ptr<int> data;
        std::string name;
    
    public:
        Modern() : data(std::make_unique<int>(0)), name("default") {}
        // コピー/ムーブ/デストラクタは自動生成に任せる
    };
    

    ---

    5. 算術・比較演算子

    5.1 算術演算子

    class Fraction {
        int num, den;
    
    public:
        Fraction(int n = 0, int d = 1) : num(n), den(d) {}
    
        // 単項マイナス
        Fraction operator-() const {
            return Fraction(-num, den);
        }
    
        // 加算
        Fraction operator+(const Fraction& other) const {
            return Fraction(
                num * other.den + other.num * den,
                den * other.den
            );
        }
    
        // 複合代入
        Fraction& operator+=(const Fraction& other) {
            num = num * other.den + other.num * den;
            den = den * other.den;
            return *this;
        }
    };
    
    // 非メンバ関数版(対称性のため推奨)
    Fraction operator+(const Fraction& a, const Fraction& b) {
        Fraction result = a;
        result += b;
        return result;
    }
    

    5.2 比較演算子

    class Point {
        int x, y;
    
    public:
        Point(int x = 0, int y = 0) : x(x), y(y) {}
    
        // 等価比較
        bool operator==(const Point& other) const {
            return x == other.x && y == other.y;
        }
    
        bool operator!=(const Point& other) const {
            return !(*this == other);
        }
    
        // 順序比較(例:辞書順)
        bool operator<(const Point& other) const {
            if (x != other.x) return x < other.x;
            return y < other.y;
        }
    
        bool operator>(const Point& other) const {
            return other < *this;
        }
    
        bool operator<=(const Point& other) const {
            return !(other < *this);
        }
    
        bool operator>=(const Point& other) const {
            return !(*this < other);
        }
    };
    

    5.3 インクリメント・デクリメント

    class Counter {
        int value;
    
    public:
        Counter(int v = 0) : value(v) {}
    
        // 前置インクリメント
        Counter& operator++() {
            ++value;
            return *this;
        }
    
        // 後置インクリメント(ダミー引数で区別)
        Counter operator++(int) {
            Counter temp = *this;
            ++value;
            return temp;
        }
    };
    

    ---

    6. ストリーム演算子

    6.1 出力演算子

    class Point {
        int x, y;
    
    public:
        Point(int x = 0, int y = 0) : x(x), y(y) {}
    
        // フレンド関数として宣言
        friend std::ostream& operator<<(std::ostream& os, const Point& p);
    };
    
    // 定義
    std::ostream& operator<<(std::ostream& os, const Point& p) {
        os << "(" << p.x << ", " << p.y << ")";
        return os;  // チェイン可能にするため
    }
    
    // 使用
    Point p(10, 20);
    std::cout << "Point: " << p << std::endl;  // Point: (10, 20)
    

    6.2 入力演算子

    class Point {
        int x, y;
    
    public:
        Point(int x = 0, int y = 0) : x(x), y(y) {}
    
        friend std::istream& operator>>(std::istream& is, Point& p);
    };
    
    std::istream& operator>>(std::istream& is, Point& p) {
        is >> p.x >> p.y;
        return is;
    }
    
    // 使用
    Point p;
    std::cin >> p;  // "10 20" と入力
    

    ---

    7. その他の演算子

    7.1 添字演算子

    class Array {
        int* data;
        size_t size;
    
    public:
        Array(size_t n) : data(new int[n]()), size(n) {}
        ~Array() { delete[] data; }
    
        // 非const版(読み書き可能)
        int& operator[](size_t index) {
            if (index >= size) throw std::out_of_range("Index out of range");
            return data[index];
        }
    
        // const版(読み取り専用)
        const int& operator[](size_t index) const {
            if (index >= size) throw std::out_of_range("Index out of range");
            return data[index];
        }
    };
    

    7.2 関数呼び出し演算子(ファンクタ)

    class Adder {
        int value;
    
    public:
        Adder(int v) : value(v) {}
    
        int operator()(int x) const {
            return x + value;
        }
    };
    
    // 使用
    Adder add5(5);
    std::cout << add5(10) << std::endl;  // 15
    

    ---

    まとめ

    本章で学んだこと:

  • 演算子オーバーロードの基礎: メンバ関数 vs 非メンバ関数
  • 代入演算子: 自己代入チェック、コピーアンドスワップ
  • コピーコンストラクタ: ディープコピーの実装
  • Orthodox Canonical Form: 4つの特殊メンバ関数
  • 各種演算子: 算術、比較、ストリーム、添字
  • 次章では、継承について学びます。

    ---

    練習問題

  • 以下のFixedクラスにOrthodox Canonical Formを実装してください:
class Fixed {
    int rawBits;  // 固定小数点数のビット表現
    static const int fractionalBits = 8;
public:
    // 4つの特殊メンバ関数を実装
};

  • コピーアンドスワップイディオムの利点を3つ挙げてください。
  • 以下のコードの問題を指摘してください:
class Resource {
    int* ptr;
public:
    Resource() : ptr(new int(0)) {}
    ~Resource() { delete ptr; }
    // コピーコンストラクタと代入演算子を定義していない
};

  • ストリーム演算子をフレンド関数として定義する理由を説明してください。