2016年4月14日木曜日

【読書ノート】Effective Modern C++ - Item 25

O'Reilly Early Releaseで購入したEffective Modern C++の読書記録です。
※この記事はあくまで私の備忘録です。(断りなく自分の解釈や考え・感想を入れたり、理由もなく内容を省略したり、間違いや曲解もあると思います。誤りの指摘は歓迎です。)

Item 25: rvalue referenceにはstd::moveを、universal referenceにはstd::forwardを使おう

std::moveとstd::forwardの使い分け

rvalue referenceは無条件にmoveして良い。
std::forwardを使っても正しい動作をするが、不自然な表現なので避けるべき。

class Widget {
public:
  Widget(Widget&& rhs) : name(std::move(rhs.name)) {…}
private:
  std::string name;
}

universal referenceの場合はrvalueで初期化された時だけrvalueにcastされなければならないので、(item23で説明した)std::forwardを使う。

class Widget {
public:
  template<typename T>
  void setName(T&& newName) {
    name = std::forward<T>(newName);
  }
  …
}

universal referenceにstd::moveを使用してはならない。

class Widget {
public:
  template<typename T>
  void setName(T&& newName) {
    name = std::move<T>(newName);
  }
  …
}
Widget w;
auto n = getWidgetName();
w.setName(n);

nはlvalueなので、w.setName(n)から戻って来た後、nはunspecified valueになってしまう。

universal referenceの優位性

以下の様な主張を言う人がいるかもしれない。
universal referenceはconstでは無いから、setNameのように変数の値を変化させない関数ではconst参照とrvalue referenceでオーバーロードするべきだと。

class Widget {
public:
  void setName(const std::string& newName)
  { name = newName; }
  void setName(std::string&& newName)
  { name = std::move(newName); }
  …
};

しかしこれは良くない。
まず、オーバーロードバージョンではw.setName("Adela Novak");とするとstd::stringの一時オブジェクトが生成されてしまい、オーバーヘッドが生じる。
std::stringのコンストラクタが呼ばれてsetNameのrvalue referenceにバインドされ、moveされ、デストラクタが呼ばれる。
universal referenceを使うバージョンでは文字列リテラルがそのまま渡されるから、std::stringの一時オブジェクトは生成されない。
次に、universal referenceを使うバージョンでは1つだった関数が2つに増えてメンテナンス性が悪くなっている。
これは引数が増えるほど問題が悪化する。引数毎にオーバーロードするなら2のべき乗で関数が増えていってしまう。
そして可変個引数の場合は不可能になり、universal referenceを使うしか無い。

template<class T, class... Args>
shared_ptr<T> make_shared(Args&&... args);
template<class T, class... Args>
unique_ptr<T> make_unique(Args&&... args);

関数内で複数回universal referenceを使う場合は最後の文にだけstd::forwardを使う必要がある。(rvalue referenceの場合はstd::move。)

template<typename T>
void setSignText(T&& text)
{
  sign.setText(text); // 渡し先でtextを壊されないようにrvalueとして渡さない
  auto now = std::chrono::system_clock::now();
  signHistory.add(now, std::forward<T>(text));
}

rvalue/universal referenceで受け取った引数を値渡しでreturnする場合

rvalue referenceで受け取った引数を値渡しでreturnする場合はstd::moveするべき。

Matrix operator+(Matrix&& lhs, const Matrix& rhs)
{
  lhs += rhs;
  return std::move(lhs);
}

lhsをmoveしないと必ずコピーが発生してしまう。
Matrixがムーブコンストラクタをサポートしている場合はstd::moveを利用することで高速化できる。
また、Matrixがムーブコンストラクタをサポートしていなくてもstd::moveの恩恵を受けられずコピーが発生するだけで動作に問題は起こらない。
従って値渡しする場合は無条件にstd::moveで返しておけば良い。

同じく値渡しでreturnする関数でuniversal referenceが引数の場合はstd::forwardで返しておけば良い。

template<typename T>
Fraction reduceAndCopy(T&& frac)
{
  frac.reduce();
  return std::forward<T>(frac);
}

rvalue referenceの場合は先ほどの例と同様で無駄なコピーを回避し、lvalue referenceの場合はコピーして返す。

RVOが適用される場合はstd::move/forwardを使ってはならない

戻り値が値渡しである関数で、戻り値にするローカル変数と戻り値の型が同じである場合はreturn value optimizatinによりコピーされることはない。
したがってRVOの条件を満たす場合はローカル変数をreturnする時にstd::moveやstd::forwardを適用しないようにする。

Widget makeWidget()
{
  Widget w;
  …
  return w; // std::moveをつけないようにする
} 

本当にRVOされるか不安な人はコピーを避けるためstd::moveを付けてしまうかもしれない。
しかしその場合はRVOは働かなくなる。std::moveはrvalue "reference"であるから戻り値の型とは異なる型となる。
結果としてムーブコンストラクタが呼ばれる分だけオーバーヘッドが生じる。
それでも確実にコピーを避けられるという考えが浮かぶかもしれないがそれは誤り。
コンパイラは、RVOが効かない場合は自動的にstd::moveを付加してreturnするようになっている。
従って戻り値の型と同一の型であるローカル変数をreturnする時にstd::moveを行うことは全く無意味であり、やってはならない。