最終更新:2026-10-12
C++ copy-and-swap イディオム¶
なぜコピーしてスワップするイディオムが必要なのでしょうか?
リソースを管理するクラス(スマートポインタなどのラッパー)は、いずれもbig threeを実装する必要があります。コピーコンストラクタとデストラクタの目的と実装はシンプルですが。
しかし、コピー代入演算子は間違いなく最も繊細で最も難しいものです。
どうすべきでしょうか?どのような落とし穴を避ける必要があるでしょうか?
copy-swapはその解決策であり、代入演算子の実装において次の二つの点で大きな助けとなります。コードの重複を避け、強力な**例外保証**を提供できるのです。
どのように動作するのでしょうか?
概念的には、コピーコンストラクタの機能を使ってデータのローカルコピーを作成し、その後スワップ機能を使ってコピーしたデータを取得し、古いデータと新しいデータを交換することで動作します。そして、一時コピーは破棄され、古いデータを一緒に持ち去ります。手元に残るのは新しいデータのコピーです。
copy-swapを使用するには、次の三つが必要です:
- 有効なコピーコンストラクタ
- 有効なデストラクタ(いずれもあらゆるラッパーの基盤であり、どうあれ完全に実装しておくべきものです)、そしてスワップ機能。
スワップ関数は例外を投げない関数で、あるクラスの二つのオブジェクトまたはメンバを交換します。独自の方法を提供する代わりにstd::swapを使いたくなるかもしれませんが、それは不可能です。std::swapはその実装の中でcopy-constructorとcopy-assignment演算子を使用しており、自分自身に基づいて代入演算子を定義しようとすることになってしまうのです!
(それだけではなく、swapへの無条件の呼び出しは、私たちのカスタムswap演算子を使用するため、std::swapを使った場合に生じる不要なクラスの構築と破棄をスキップできます。)
具体的な例は次のとおりです:
namespace A {
template<typename T>
class smart_ptr {
public:
smart_ptr() noexcept : ptr_(new T()) {
}
smart_ptr(const T &ptr) noexcept : ptr_(new T(ptr)) {
}
smart_ptr(smart_ptr &rhs) noexcept {
ptr_ = rhs.release(); // 释放所有权,此时rhs的ptr_指针为nullptr
}
void swap(smart_ptr &rhs) noexcept { // noexcept == throw() 保证不抛出异常
using std::swap;
swap(ptr_, rhs.ptr_);
}
T *release() noexcept {
T *ptr = ptr_;
ptr_ = nullptr;
return ptr;
}
T *get() const noexcept {
return ptr_;
}
private:
T *ptr_;
};
// 提供一个非成员swap函数for ADL(Argument Dependent Lookup)
template<typename T>
void swap(A::smart_ptr<T> &lhs, A::smart_ptr<T> &rhs) noexcept {
lhs.swap(rhs);
}
}
// 注释开启,会引发ADL冲突
//namespace std {
// // 提供一个非成员swap函数for ADL(Argument Dependent Lookup)
// template<typename T>
// void swap(A::smart_ptr<T> &lhs, A::smart_ptr<T> &rhs) noexcept {
// lhs.swap(rhs);
// }
//
//}
int main() {
using std::swap;
A::smart_ptr<std::string> s1("hello"), s2("world");
// 交换前
std::cout << *s1.get() << " " << *s2.get() << std::endl;
swap(s1, s2); // 这里swap 能够通过Koenig搜索或者说ADL根据s1与s2的命名空间来查找swap函数
// 交换后
std::cout << *s1.get() << " " << *s2.get() << std::endl;
s1=s2;
}
さて、上記のs1=s2を完動させるには、代入演算子を実装しなければなりません。
方法1
自己代入を避けるために、通常は次のような書き方が用いられます。
ダメです!
例外安全ではなく、自己代入の安全性のみを備えています
smart_ptr &operator=(const smart_ptr &rhs) {
if (*this != rhs) {
delete ptr_;
ptr_ = new T(rhs.ptr_); // 当new 发生异常,此时ptr_指向的而是一块被删除区域,而不是被赋值对象的区域
return *this;
}
return *this;
}
方法2
newで例外が発生しても、ptr_は元のまま保たれます! 自己代入にも対応できます! しかし、まだ十分ではありません!
これではコードが膨張し、別の問題を引き起こします。コードの冗長性です
// 方法2:如果new出现异常,ptr_会保持原装! 也可以处理自我赋值! 还是不够好!
smart_ptr &operator=(const smart_ptr &rhs) {
T *origin = ptr_;
ptr_ = new T(rhs.ptr_);
delete origin;
return *this;
}
方法3
copy and swap は素晴らしい!
方法4
値渡しに変更したもので、方法3と同じです!
C++11 move
big threeにmove ctorとmove assignmentを加えると、big fiveが構成されます。
このとき、上記のコードをさらに拡張します:
// move ctor
smart_ptr(smart_ptr &&rhs) noexcept {
std::cout << "move ctor" << std::endl;
ptr_ = rhs.ptr_;
if (ptr_)
rhs.ptr_ = nullptr;
}
// move assignment
smart_ptr &operator=(smart_ptr &&rhs) noexcept {
std::cout << "move assignment" << std::endl;
smart_ptr tmp(rhs);
swap(rhs);
return *this;
}
実のところ、move assignmentのために余分なコードを書く必要はなく、copy-and-swapテクニックとmove-and-swapテクニックは同じ関数を共有しています。**copyコンストラクタが上記の方法4のような形のとき、**C++11では、コンパイラが引数が左値か右値かに応じて、コピーコンストラクタとムーブコンストラクタのどちらを使うかを選択します:
そのため、これが上記で書いた次のコード
と同時に存在すると、error: ambiguous overload for ‘operator=’ が発生します。
呼び出し側は次のとおりです:
A::smart_ptr<std::string> s1("hello"), s2("world");
A::smart_ptr<std::string> s3 = s1;
A::smart_ptr<std::string> s4 = std::move(s1);
- s3 = s1 の場合は、(s1が左値であるため)コピーコンストラクタが呼び出されてotherが初期化され、代入演算子は新しく作成されたオブジェクトとデータを交換します。つまりディープコピーです。これがcopy and swapイディオムの定義です。コピーを構築し、そのコピーとデータを交換し、コピーはスコープを抜けるときに自動的に破棄されるようにします。
- s4 = std::move(s1) の場合は、(std::move(s1)が右値であるため)ムーブコンストラクタが呼び出されてrhsが初期化されます。したがって、ここではディープコピーは発生せず、効率的なデータの転送のみが行われます。
そのため、これは「コピー代入」と「ムーブ代入」を統合するものであり、「統一代入演算子」と呼ぶこともできます。
関連トピック¶
- C++ デフォルトコンストラクタの回避 | C++那些事
- C++ クラス初期化リストイディオム | C++那些事
- C++ 関数テンプレートよりクラステンプレート | C++那些事
- C++ 依存性注入 | C++那些事
- C++ enum class 強い型付けされた列挙 | C++那些事
- C++ Header-Only と実装分離 | C++那些事
- C++ 遅延評価イディオム | C++那些事
- C++ ルックアップテーブル法イディオム | C++那些事
- C++ メンバポインタと型情報 | C++那些事
- C++ Mixin イディオム | C++那些事