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を行うことは全く無意味であり、やってはならない。