自定义データ型に対して自然な演算子記法を提供するには、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引数をストリーム参照とする