C++クラスにおける演算子オーバーロードと特殊メンバ関数の実装

自定义データ型に対して自然な演算子記法を提供するには、C++の演算子オーバーロード機能が不可欠です。本稿では、比較演算子、代入演算子、ストリームI/O演算子の実装に加え、constメンバ関数やアドレス取得演算子の振る舞いについて、実践的なコード例とともに解説します。

日付比較のための等価性演算子の定義

組み込み型は==で直接比較できますが、ユーザー定義クラスでは明示的な比較ロジックが必要です。以下はDateクラスに対する二項operator==のグローバル定義例です。

#include <iostream>

struct Date {
    int year, month, day;
    Date(int y, int m, int d) : year(y), month(m), day(d) {}
};

bool operator==(const Date& lhs, const Date& rhs) {
    return lhs.year == rhs.year &&
           lhs.month == rhs.month &&
           lhs.day == rhs.day;
}

int main() {
    Date d1(2024, 5, 15), d2(2024, 5, 15);
    std::cout << std::boolalpha << (d1 == d2) << '\n'; // true
}

クラス内での演算子オーバーロード

プライベートメンバーへのアクセスを許可するため、演算子をクラス内に定義する場合、暗黙のthisポインタを考慮したシグネチャになります。

class Date {
    int year, month, day;
public:
    Date(int y, int m, int d) : year(y), month(m), day(d) {}

    // メンバ関数としての等価比較(左辺は*this)
    bool operator==(const Date& other) const {
        return year == other.year &&
               month == other.month &&
               day == other.day;
    }
};

代入演算子の正しい実装

代入演算子は常に参照を返す必要があります。これによりa = b = cのような連鎖代入が可能になります。また、自己代入の検出も重要です。

class Date {
    int year, month, day;
public:
    Date& operator=(const Date& src) {
        if (this == &src) return *this; // 自己代入防止
        year = src.year;
        month = src.month;
        day = src.day;
        return *this;
    }
};

constメンバ関数の設計原則

const修飾されたメンバ関数は、オブジェクトの状態を変更できません。ただしmutableで宣言されたメンバーは例外です。

class Counter {
    mutable int access_count = 0;
    int value;
public:
    Counter(int v) : value(v) {}

    // const版:状態を変更しないが、mutableメンバーは更新可能
    int get_value() const { 
        ++access_count; // OK: mutableメンバー
        return value; 
    }

    // 非const版:通常の状態変更が可能
    void set_value(int v) { value = v; }
};

アドレス演算子のオーバーロード

コンパイラはデフォルトでoperator&を生成しますが、特殊な制御が必要な場合は明示的にオーバーロード可能です。const/non-constバージョンを分けて定義できます。

class SecureData {
    int data;
public:
    SecureData(int d) : data(d) {}

    // 非constオブジェクト用:nullptrを返す(アクセス制限)
    SecureData* operator&() { return nullptr; }

    // constオブジェクト用:実際のアドレスを返す
    const SecureData* operator&() const { return this; }
};

ストリーム演算子の友元関数による実装

<<および>>はクラスの外部からストリームオブジェクトにアクセスする必要があるため、通常はfriend関数として定義します。戻り値はストリーム参照で、チェイン化をサポートします。

#include <iostream>

class Vector2D {
    double x_, y_;
public:
    Vector2D(double x = 0.0, double y = 0.0) : x_(x), y_(y) {}

    friend std::istream& operator>>(std::istream& is, Vector2D& v) {
        is >> v.x_ >> v.y_;
        return is;
    }

    friend std::ostream& operator<<(std::ostream& os, const Vector2D& v) {
        os << '[' << v.x_ << ", " << v.y_ << ']';
        return os;
    }
};

// 使用例:
// Vector2D v; std::cin >> v; std::cout << v << '\n';

制限事項とベストプラクティス

  • sizeof?:..*::はオーバーロード不可
  • 演算子オーバーロード関数は少なくとも1つの引数がユーザー定義型である必要がある
  • 代入演算子は必ずクラスのメンバ関数として実装する
  • constメンバ関数内で非mutableメンバーを変更しようとするとコンパイルエラー
  • ストリーム演算子は友元関数または非メンバ関数で実装し、第1引数をストリーム参照とする

タグ: cpp operator-overloading const-member-function stream-operators assignment-operator

6月26日 19:34 投稿