rvalue reference
最近になってrvalue referenceというものを知りました。
まだきちんと理解できていない可能性もありますが、一応メモ書き。
私は今まで以下のようなオブジェクトそのものを値返却する関数の作成を避けていました。
class A { public: A() : v(1000000) {} std::vector<int> v; }; A f() { A a; a.v[10] = 10; a.v[100] = 100; //... return a; } void g() { A a = f(); }
なぜなら、昔のC++では、関数g内のaを、関数fの戻り値で初期化する際に、暗黙のコピーコンストラクタによって、vectorの中身すべてをコピーする処理が走っていたためです。ある関数の中でオブジェクトを生成して返却するような場合、以下のように呼び出し側でオブジェクトを用意して、引数で参照やポインタを渡すようにしてきました。
class A { public: A() : v(1000000) {} std::vector<int> v; }; void f(A& a) { a.v[10] = 10; a.v[100] = 100; //... } void g() { A a; f(a); }
しかし、今(C++0x以降)のC++では、このようなことをする必要はないそうです。
lvalueとrvalue
変数はlvalueです。
一時オブジェクトや関数の戻り値のような、2度と使われない値はrvalueです。
struct B {}; int f() {return 0;} void g(B b) {} int x = 1; // xはlvalue, 1はrvalue x = f(); // f()はrvalue g(B()); // B()はrvalue
rvalue参照
rvalue参照は、2つのアンパサンド(&&)で表します。
【rvalueを受け取る代入演算子】
class C { public: C& operator=(C&& r) {this->ptr = r.ptr; r.ptr = nullptr; return *this;} int* ptr; };
rvalueは2度と使われることのない値なので、中身のポインタをかっさらってしまっても問題ない。これにより、高速な値の受け渡しができる。ということです。
std::move
move関数を使うと、lvalue参照をrvalue参照に変換することができます。
move関数は、「この変数はもう2度と使わないよ」という宣言であり、内部的には&&へのstatic_castです。
最初の例の関数fの戻り値は明らかにrvalueなので、以下のように書けるということになります。
class A { public: A() : v(1000000) {} A(A&& r) {this->v = std::move(r.v); printf("called rvalue constructor.");} std::vector<int> v; }; A f() { A a; a.v[10] = 10; a.v[100] = 100; //... return std::move(a); } void g() { A a = f(); }
ここで、関数fの戻り値は、コンパイラから見て明かにrvalueなので、moveする必要はありません。
なので、通常関数の戻り値に対して、moveを書くことはありません。
(書いてしまっても特に問題はありませんが、後述のように挙動は多少変わる可能性があります。)
NRVO
ところで、Xcode 4.5.2(Clang 4.0)で上記のコードを動かすと、期待通りコンソールに
called rvalue constructor.
と表示されます。
が、本来必要のない戻り値に対するmoveを外すと、コンソールには何も表示されなくなってしまいました。その理由がわからずにしばらく色んなサイトで情報を探したところ、この例のような場合には、NRVO(Named Return Value Optimization)というコンパイラの最適化が入るそうです。NRVOやRVOの最適化が入ると、返却値をコピーコンストラクタに渡すどころか、そもそも呼び出し元の値にそのまま返却値を突っ込んでしまう(代入やコピーコンストラクタせずに、コンパイラが同じメモリを自動的に使い回すようにしてくれる)ので、rvalue参照のコピーコンストラクタすら必要ないということになります。
(ただし、最適化されるかどうかはコンパイラ依存なので、移植性を考えるならばrvalue参照コンストラクタとoperator=()はやはりあった方が良い)
ということは、つまり、最初のコード
class A { public: A() : v(1000000) {} std::vector<int> v; }; A f() { A a; a.v[10] = 10; a.v[100] = 100; //... return a; } void g() { A a = f(); }
これのままで、現代的なC++としては、既に無駄のないコードだった、というわけですね。
時代は変わったものだ・・・
しかし、こうなってくると、もはやコードの表面上に&&も出て来ないし、std::moveも出て来ないし、私のような旧式オサーンプログラマには、ぱっと見では中で何が起きるのかさっぱり分からないですね。。。
実際の開発現場で、若者が皆このような書き方をしたとして、
「このコードはなっちょらん!」
とか言って、ストールマンみたいな風貌のGさんが、片っ端からCスタイルのポインタ引数形式に修正し始めたらどうするんだ。しかもこれ、1世代前に取り入れられたC++規格だぜ。C++11どころじゃなく、完全に時代に取り残された。
私にはもはや、こんなに難しい言語は使いこなせないのではなかろうか・・・
いや・・・
まだだ・・・まだ戦えるぅ・・・・・orz