C++ 11 の rvalueリファレンス
はじめに
C++ 11 の最大の変更は rvalue参照(rvalue reference)が追加されたことです
rvalue参照とは何でしょうか
まとめ
先に結論を言ってしまうと、rvalue参照というのは破壊可能な参照のことです
なぜ破壊可能かというとそれは破壊可能なオブジェクトを参照しているからです
当たり前ですが重要なところです
破壊可能なオブジェクトとは何かと言うとそれは一時オブジェクトのことです
一時オブジェクトというのは式の途中で一時的に作られて役割を終えればすぐに消滅する運命にあるものです
一時オブジェクトの役割というのはそこにあるということとそれを誰かに伝えるということです
rvalue参照として渡されてきたそのこと自体が役割なので渡されてきた一時オブジェクトは既に役割を終えています
役割を終えた一時オブジェクトは後は消えてしまうだけのものなのだから破壊してしまっても構わないというわけです
この一時オブジェクトのことを rvalue と言います
rvalueを参照しているものだから rvalue参照と言います
つまり、rvalue参照というのは破壊可能な参照のことです
破壊可能な参照
破壊可能な参照というのは破壊してしまっても構わないオブジェクトを参照しているものということですが、破壊してしまっても構わないというだけで必ず破壊しなければいけないわけではありません
しかし破壊しないのなら rvalue参照である意味がありません
rvalue参照には通常は const を付けません
// rvalue参照 void bar( cfoo&& foo ){ // foo から必要な情報を取りだしたら破壊する ... }
文法的には単純に constが付いていないから破壊可能であるというだけです
別に rvalue参照が特別な何かというわけではありません
constが付いていたら rvalue参照であっても破壊できません
// const な rvalue参照 void bar( const cfoo&& foo ){ // foo から情報を得ることはできるが foo は const なので破壊できない ... }
rvalue参照は参照先を破壊したいときに使うものですから const を付けてはいけません
const な rvalue参照
const な rvalue参照というものも文法上は存在します
しかし const な rvalue参照には価値がありません
const な rvalue参照にも使い道は無くはないのですが基本的には一切使いません
rvalue参照であれ lvalue参照であれ constな参照を使うということは参照先のオブジェクトを破壊しないという意志表明です
rvalue参照は破壊可能であるという点において存在意義があるものなのにそれを破壊しないというなら rvalue参照を使う意味がありません
const を付けたいときは rvalue参照なんかを使わずに今までの const参照を使えば済むことです
// const参照 void bar( const cfoo& foo ){ // foo が lvalue でも rvalue でも関係なく foo から情報を得ることができる ... }
今までの const参照は lvalue を参照することもできますし rvalue を参照することもできます
const な rvalue参照の機能を完全に含んでいます
参照先を破壊しないのなら参照先が lvalue でも rvalue でもどちらでも関係ないのです
rvalue参照は参照先を破壊したいときだけ使うようにしましょう
ところで参照先を破壊するとは何のことでしょうか
それを理解するためにはじめに戻って一から rvalue参照について説明しましょう
rvalueとは
rvalue参照の参照する rvalue というのは一時オブジェクト(temporary object)のことですと言いました
そもそも rvalue とは一体何のことでしょうか
単に rvalue と言った場合はイミディエイトなども含みますが、rvalue reference の rvalue とは一時オブジェクトのことです
例えば次のような宣言で a は 10 という抽象的な即値を参照しているというよくわからない状況にあるわけではありません
a は 10 が入っている具体的な一時オブジェクトを参照しています
int&& a = 10;
一時オブジェクト
一時オブジェクトというのはソースコード上で明示的には書かれていないけれどもそこにあるもののことです
オブジェクトの型を暗黙に変換するとき、関数に引数を渡すとき、関数から戻り値を受け取るときなど式の途中に発生し、通常は完結式(full-expression)の終わりで消滅します
12 Special member functions 12.2 Temporary objects 3. ... Temporary objects are destroyed as the last step in evaluating the full-expression (1.9) that (lexically) contains the point where they were created. ...
下記のコードで一時オブジェクトが発生していています
std::string get_foo(){ return "foo"; } std::string foo = get_foo();
上記のコードを展開してみると次のようになっています
std::string return_value("foo");
std::string foo( return_value );
return_value が一時オブジェクトです
return_value を foo にコピーしているところが無駄ですね
const参照
一時オブジェクトのコピーは無駄なので、一時オブジェクトをそのまま使えるようにする文法があります
それが const参照です
一時オブジェクトは完結式の終わりで消滅すると言いましたが、const参照で縛ることでその寿命を延長することができます
std::string get_foo(){ return "foo"; } const std::string& foo = get_foo();
上記のコードを展開してみると次のようになっています
std::string return_value("foo"); const std::string& foo = return_value;
無駄なコピーを消すことができました
消せない無駄なコピー
上記のような例では無駄なコピーを消すことができましたが、次のような例ではどうでしょうか
std::string get_foo(){ std::string temp("foo"); return temp; } const std::string& foo = get_foo();
このコードを展開してみると次のようになります
std::string temp("foo"); std::string return_value( temp ); temp を破棄 const std::string& foo = return_value;
temp を return_value にコピーしているところが無駄ですね
ここも const参照にできるでしょうか
const std::string& get_foo(){ std::string temp("foo"); return temp; } const std::string& foo = get_foo();
↓
std::string temp("foo"); const std::string& return_value = temp; temp を破棄 const std::string& foo = return_value;
しかし、こんなことはできませんね
temp の寿命は関数の終わりまでなのでそれを超えて参照を返せたとしても意味がありません
寿命を超えて値を返すためにはコピーが必要です
本当にそうでしょうか
コピーが必要か
C++ の常識で考えるとコピーするしかないのですが、アセンブラで考えてみましょう
戻り値のためのスタック領域の場所を先に決めておけばその関数内で戻り値用のオブジェクトをそもそもその場所に作っておけば済むことで別にコピーする必要はありません
ですから必ずしもコピーの必要はありません
関数の戻り値をコピーしなければいけないというのは絶対の問題なのではなく C++ の仕様の問題なのです
仕様に問題があるなら仕様を変えてしまえばいいだけのことですね
C++ 11 で導入された解決策は上記とはまた別の方法でした
C++ 11 で導入された解決策は、一切コピーしないではなく少しだけコピーする。というものでした
少しだけコピーする
例えば std::string は内部に文字列を入れておくバッファを指すポインタや長さを入れておく整数などを持っています
ここで重いのは文字列をコピーする部分です
もし文字列をコピーしなくて済むなら、コピーが発生したとしてもその負荷はかなり軽減されます
string でやっていそうな処理を見てみましょう
class string { char* m_data; size_t m_size; size_t m_capacity; void destroy(){ if( m_data ){ delete[] m_data; m_data = NULL; } m_size = 0; m_capcity = 0; } public: string( const char* s ): m_data(), m_size(strlen(s)), m_capcity(m_size+1) { m_data = new char[m_capacity]; memcpy( m_data, s, m_capacity ); } string( const string& rhs ): m_data(), m_size(rhs.m_size), m_capcity(rhs.m_capacity) { m_data = new char[m_capacity]; memcpy( m_data, rhs.m_data, m_capacity ); } string& operator=( const string& rhs ){ if( this != &rhs ){ destroy(); m_capcity = rhs.m_capacity; m_size = rhs.m_size; m_data = new char[m_capcity]; memcpy( m_data, rhs.m_data, m_capacity ); } return *this; } ~string(){ destroy(); } };
そしてこの処理内容を使って上記にもあった次のコードをさらに展開してみましょう
std::string get_foo(){ std::string temp("foo"); return temp; } const std::string& foo = get_foo();
↓
std::string temp("foo"); std::string return_value( temp ); const std::string& foo = return_value;
↓
// std::string temp("foo"); const char* s = "foo"; temp.m_size = strlen(s); temp.m_capcity = temp.m_size+1; temp.m_data = new char[temp.m_capcity]; memcpy( temp.m_data, s, temp.m_capcity ); // std::string return_value( temp ); return_value.m_size = temp.m_size; return_value.m_capacity = temp.m_capacity; return_value.m_data = new char[return_value.m_capacity]; memcpy( return_value.m_data, temp.m_data, return_value.m_capacity ); // temp を破棄 if( temp.m_data ){ delete[] temp.m_data; temp.m_data = NULL; } temp.m_size = 0; temp.m_capcity = 0; // そのまま const std::string& foo = return_value;
こうしてみると無駄な処理がよくわかりますね
temp の中身を新たに確保した return_value にコピーしていますが、temp はそのすぐ後に破棄していますから別にコピーしなくても temp が持っていたポインタを return_value にあげれば済むことのように見えます
つまりこのような処理になっていると無駄がなく理想的です
// std::string temp("foo"); const char* s = "foo"; temp.m_size = strlen(s); temp.m_capcity = temp.m_size+1; temp.m_data = new char[temp.m_capcity]; memcpy( temp.m_data, s, temp.m_capcity ); // std::string return_value( temp ); return_value.m_size = temp.m_size; return_value.m_capacity = temp.m_capacity; return_value.m_data = temp.m_data; // どうせ捨てるのでポインタをそのままあげる temp.m_data = NULL; // temp のデストラクタで解放されてしまうと困るので temp が持っていた元のポインタは NULL にしておく temp.m_size = 0; temp.m_capacity = 0; // temp を破棄 if( temp.m_data ){ delete[] temp.m_data; temp.m_data = NULL; } temp.m_size = 0; temp.m_capcity = 0; // そのまま const std::string& foo = return_value;
どうしてこのようになっていないのでしょうか
それはポインタをあげているところの元のコードを考えるとわかります
元のコードは string のコピーコンストラクタです
コピーコンストラクタの引数は const参照です
const参照が参照している先は temp なので別に壊しても構わないのですが、文法上そういったことはできません
もちろん const_cast を使えば無理やり書き変えることはできます
string( const string& rhs ): m_data(rhs.m_data), m_size(rhs.m_size), m_capcity(rhs.m_capacity) { const_cast<string&>(rhs).m_data = NULL; const_cast<string&>(rhs).m_size = 0; const_cast<string&>(rhs).m_capacity = 0; }
しかしそんなことをすると利点よりも大きな欠点を抱えることになるのは明らかです
std::string a("foo"); std::string b(a); // a が壊れる
参照の先にあるものを壊していいかどうかの情報を得る方法が何かないでしょうか
そういったものがあれば無駄なコピーをしなくて済むようにできるのですが
rvalue参照
正にこういった問題を解決するために C++ 11 に導入されたものが rvalue参照です
rvalue参照は参照先にあるものを壊していい参照です
ですから rvalue参照を受け取るコピーコンストラクタでは参照先を壊して構いません
string( string&& rhs ): m_data(rhs.m_data), m_size(rhs.m_size), m_capcity(rhs.m_capacity) { rhs.m_data = NULL; rhs.m_size = 0; rhs.m_capacity = 0; }
C++ 11 ではこの rvalue参照を受け取るコピーコンストラクタをムーブコンストラクタと呼んでいます
rvalue参照を受け取るコピー代入演算子も同様にムーブ代入演算子と呼びます
moveとは
C++ 11 ではコピー元を壊すコピーということでムーブ=移動と言っています
ここはときどき勘違いする人がいるのですが、先にも言った通り C++ 11 で導入されたのは「一切コピーしない」ではなく「少しコピーする」です
rvalue参照を使うとコピーをしないで変数のアドレスだけを変えてくれるなどということはありません
コピーはします
上記のムーブコンストラクタの例を見ても実際にはいくつかのメンバをコピーをしていますよね
コピーと移動の違いは、コピーするかしないかではなく、参照元を壊すか壊さないかです
そういう意味じゃない
ですから、スタックに置いてあった A という変数が突然ヒープに移動したなんてことは起きません
当たり前のことですが変数のアドレス &A が変化することはありません
移動というのはそういう意味ではありません
std::move
何が lvalue で何が rvalue かということは文法によって決まっているものですが、プログラマが明示的に指定することもできます
rvalue を lvalue にするには名前を付けてあげます
それだけです
名前の付いているものはすべて例外なく lvalue になります
cfoo get_foo( int ); cfoo&& foo = get_foo( 100 );
foo は rvalue参照なので foo の参照している先は rvalue ですが、foo 自身は foo という名前を持っているので lvalue です
lvalue を rvalue として扱ってもらうには rvalue参照にキャストします
cfoo bar = static_cast<cfoo&&>(foo); cfoo baz; baz = static_cast<cfoo&&>(bar);
これは何をしているのかというと cfoo::cfoo( cfoo&& ) や cfoo::operator=( cfoo&& ) を明示的に呼び出しているというだけです
cfoo::cfoo( cfoo&& ) や cfoo::operator=( cfoo&& ) を呼び出すということはその処理によって中身を変更してもいいよと表明しているということです
つまり rvalue として扱ってほしいということはもう使わないから中身を好きに書き変えてもいいよと言っているのと同じことです
標準ライブラリではこの表明に std::move という名前を付けています
std::move は rvalue参照への static_cast と同じです
moveした後
move した後に渡したオブジェクトに触っていいのかということが気になる人がいますが、それは因果が逆です
もう触らないから move するのです
まだ触りたいなら move してはいけません
移動というのは特別な何かではなく文法上はただの破壊的なコピーなので触ってはいけない理由はありません
char* strmove( char* d, char*s ){ char* result = d; while(( *d = *s ) != 0 ){ *s = 0; ++s; ++d; } return result; } char a[10]; char b[10] = "bbb"; strmove( a, b );
このコードで strmove の後に b に触っていいのかというのと同じことです
触ってはいけない理由はありません
意味のない move
移動というのはメンバ変数にポインタを持っているような場合にだけ有効に機能します
配列などはコピーするしかないのですからコピーと移動を分ける意味がありません
class cfoo { char m_data[1000]; public: cfoo( cfoo&& ){ ... } }; class cbar { int m_aaa; int m_bbb; int m_ccc; int m_ddd; int m_eee; public: cbar( cbar&& ){ ... } };