「 Copy して Swap 」 対 「 Copy して Move 代入」

例外安全なコピー代入演算子を定義しようとしたとき, 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 した場合と比較し,無駄な処理が少ない


デメリット:

  • Move 代入演算子の定義を忘れると無限ループ
  • Move 代入を = default; で定義できない場合も しばしば存在し,そのような場合,
    • メンバ変数の追加に弱い
    • Copy/Move コンストラクタとの一貫性を維持しにくい
  • 状況に応じて例外指定を行う必要がある
  • Move 代入演算子が例外を投げうる場合には全く意味が無い

使い分け基準の提案

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を使う場合


ともあれ,例外を投げうるコピーおよびムーブならびに代入操作は滅ぶべきであると考える次第である.

*1:デストラクタが絡む場合の他には, Move 後のオブジェクトに対しても不変条件が満たされることを保証したい場合にも,一般に Move 操作はユーザ定義する必要がある

*2:GCC だと 4.4 以前