導入
C++の実務開発において、コピー代入演算子(operator=)の実装は、メモリ管理や例外安全性の観点から非常に注意が必要な作業です。特に、動的メモリ確保を伴うクラスでは、自己代入への対処や、例外発生時のリソースリーク防止といった「強い例外安全性」を考慮しなければなりません。Copy-and-Swapイディオムは、これらの課題をコードの冗長さを抑えつつ、極めて安全に解決するための標準的な設計パターンです。
基礎知識
Copy-and-Swapイディオムを理解するためには、C++の「コピーコンストラクタ」「デストラクタ」「スワップ(交換)」の3つの概念が不可欠です。
コピーコンストラクタは、既存のオブジェクトから新しいオブジェクトを生成します。
デストラクタは、オブジェクトの生存期間が終了する際にリソースを解放します。
スワップは、2つのオブジェクトが持つ内部リソースの所有権を効率的に入れ替える操作を指します。このイディオムでは、コピーした一時オブジェクトと自身を入れ替えることで、複雑なリソース解放と再確保の手順を自動化します。
実装/解決策
このイディオムの鍵は「値渡し」にあります。引数を参照ではなく値(MyClass other)で受け取ることで、代入の直前に自動的にコピーコンストラクタが呼ばれます。
1. 引数として渡された時点で、新しいリソースが確保された一時オブジェクトが生成されます。
2. メンバ関数のswapを呼び出し、自身の持つリソースと一時オブジェクトのリソースを入れ替えます。
3. 関数を抜ける際、一時オブジェクトのデストラクタが自動的に呼ばれ、自身の「古いリソース」が安全に解放されます。
これにより、例外が発生しても自身の状態は変更されず、自己代入のチェックも不要になるというメリットがあります。
サンプルプログラム
以下に、動的配列を管理するクラスでの実装例を示します。
include <iostream>
include <algorithm> // std::swap用
class MyBuffer {
int data;
size_t size;
public:
MyBuffer(size_t n) : data(new int[n]), size(n) {}
~MyBuffer() { delete[] data; }
// コピーコンストラクタ
MyBuffer(const MyBuffer& other) : data(new int[other.size]), size(other.size) {
std::copy(other.data, other.data + size, data);
}
// friend宣言したswap関数
friend void swap(MyBuffer& first, MyBuffer& second) noexcept {
using std::swap;
swap(first.data, second.data);
swap(first.size, second.size);
}
// Copy-and-Swapによる代入演算子
MyBuffer& operator=(MyBuffer other) noexcept {
// 値渡しでコピー済みのotherと自身のデータを交換
swap(this, other);
// 関数を抜けるとき、otherが破棄され、古いデータも安全に解放される
return this;
}
};
応用・注意点
この手法を用いる際は、クラス内に適切に実装された swap関数 が存在することが前提となります。また、クラスが所有するリソースがコピー不可能な場合(std::unique_ptrなど)には適用できません。
注意点として、値渡しを行うため、コピーコストが高いオブジェクトの場合には、ムーブセマンティクスを併用することでさらなる最適化が可能です。例外安全性と保守性のバランスを考えると、現代のC++開発において、このイディオムはクラス設計の基本ルールとして積極的に採用すべき手法です。

コメント