例外安全なコピー代入演算子を定義しようとしたとき, C++11 では
- Copy して Swap する方法
- Copy して Move 代入する方法
の二通りが存在するので,それぞれのメリットとデメリットを比べてみた.
Copy して Swap
昨日の記事で説明した方法.
struct Hoge { std::vector<int> x, y; Hoge() = default; Hoge(std::vector<int> x_, std::vector<int> y_) : x(std::move(x_)), y(std::move(y_)) {} Hoge(Hoge const&) = default; Hoge(Hoge &&) = default; Hoge& operator=(Hoge rhs /*pass by val*/) noexcept { this->swap(rhs); return *this; } void swap(Hoge& other) noexcept { using std::swap; swap(x, other.x); swap(y, other.y); } friend void swap(Hoge& l, Hoge& r) noexcept { l.swap(r); } };
メリット:
- 代入演算子を一つ定義すれば済む
- C++98/03 でも全く同じように書ける
- メンバ変数が Move 代入をサポートしていない場合でも, Swap さえあれば問題ない
- Copy/Move コンストラクタと Copy/Move 代入演算子の挙動が自然に一致する
デメリット:
- 手動で Swap を定義しなければいけない(メンバ変数の追加に弱い)
- 単純な Move 代入と比べて処理が多くなる傾向がある(最適化を考慮しない場合)
Copy して Move 代入
過去の記事で提案した方法.
struct Hoge { std::vector<int> x, y; Hoge() = default; Hoge(std::vector<int> x_, std::vector<int> y_) : x(std::move(x_)), y(std::move(y_)) {} Hoge(Hoge const&) = default; Hoge(Hoge &&) = default; Hoge& operator=(Hoge&&) = default; // デフォルト定義された Move 代入演算子 Hoge& operator=(Hoge const& rhs) { *this = Hoge(rhs); // コピーコンストラクタを呼び,その結果を Move 代入 return *this; // return *this = Hoge(rhs); と,一行で書くこともできる } };
メリット:
- 自動生成された代入演算子を使うため,メンバ変数の追加に強い
- 自前の Swap を用意する必要がない(汎用の std::swap で十分に効率的)
- Copy して Swap した場合と比較し,無駄な処理が少ない
デメリット:
使い分け基準の提案
Copy して Move 代入するケース
- メンバ変数が多い,またはメンバ変数構成が変更される可能性が高い場合
- ただし,コピー操作が例外を投げない場合,またはメンバ変数が一つしか存在しない場合には,
Copy/Move 関連のコンストラクタ/代入演算子は明示的に書かず,自動生成されたものを使う方が良い
Copy して Swap するケース
- Copy/Move コンストラクタを = default; で定義できない場合(典型的にはデストラクタをユーザ定義する場合*1)
- Swap が例外を投げないことは分かってるが, Copy が例外を投げうるか否かが よく分からない場合
- メンバ変数が Swap には対応しているが Move には対応していない場合(C++98/03時代のクラスを使う場合)
- C++11 ではなく C++98/03 を使う場合,または Move 代入に対する = default; 指定に対応していないコンパイラ*2を使う場合
ともあれ,例外を投げうるコピーおよびムーブならびに代入操作は滅ぶべきであると考える次第である.