しるふぃずむ

どうもプログラマです。好きなものはC#とジンギスカンです。嫌いなものはJava。プログラムおもちろいね。

Rvalue references

「右辺値参照」
Ideone.com - 9GodmI - Online C++0x Compiler & Debugging Tool
理解が正確かどうかはかなり怪しいですが,自分流の解釈を.

  • lvalue(左辺値)≒識別子と対応しているオブジェクト
  • rvalue(右辺値)≒一時オブジェクト,識別子と結びついていないオブジェクト

というように捉えていいかと思います.
少なくとも従来のC++では,ということになりますが後述.

従来の「参照」は基本的にはlvalue(左辺値)しかとることができませんでした.
例外的にconst reference(const&)にはrvalueを渡すこともできますが,基本的には一時オブジェクトへの参照は意味のないものとされていたようです.

しかし,それができると嬉しい状況がいくつか知られるようになりました.
コピーのコストが重いオブジェクトを扱う際,コピー元を破棄してもよいものとするとコストを大きく抑えられるケースがあります.
例えばstd::vectorのようなものを考えると,単純にコピーするには保持している全ての要素をコピーしなければなりません.
コピー元が一時オブジェクトであれば,コピーが行われた後でコピー元のオブジェクトが破棄,解放されることになります.
ここでコピー元をもう使わないということが分かっていれば新たに領域を確保することも一つ一つコピーすることも無く,ただ古い方から新しいオブジェクトへポインタを繋ぎかえることで必要な動作は達成できます.この考え方をムーブセマンティクス(所有権の移動)と呼びます.

C++03の範囲でもこの動作は実現することができ,幾つかのライブラリ内部処理やauto_ptr等に実用されています.
しかし,通常の参照を用いて実装しているために元の変数を破壊していることが判り難く,注意深く用いる必要がありました.
これを文法レベルで明示すること,また利用しやすくすることがrvalue reference導入のモチベーションの一つでしょう.

C++11ではrvalueへの参照型が追加されており,&&での修飾でこれを表します.
対して従来の参照(&)をlvalue referenceと呼び,区別しています.
基本的にlvalue referenceはlvalueを,rvalue referenceはrvalueを参照することができます*1

int func(){ return 0; }
struct Foo{};

int main(int,char*[])
{
	int x = 1;
	Foo foo{};

	x;  // lvalue
	1;  // rvalue(prvalue)
	
	func(); // rvalue(prvalue)

	foo;    // lvalue
	Foo{};  // rvalue(prvalue)

	// lvalue reference
	int& lr1 = x; 
	// int& lr2 = 1; // ERROR

	// const lvalue reference
	int const& clr1 = x;
	int const& clr2 = 1;

	// rvalue reference
	// int&& rr1 = x; // ERROR
	int&& rr2 = 1;

	// const rvalue reference
	// int const&& crr1 = x; // ERROR
	int const&& crr2 = 1;

	...
	return 0;
}

定数値や関数の戻り値など,変数と結び付けられていない値がrvalue,特にprvalueとなります.pureなrvalueということでしょうか.

そして,既に変数に束縛されているオブジェクトの所有権を他へ渡したい時にはキャストを用います.rvalue reference型へstatic_castすればそれで良いのですが,少々面倒な記述なので煩雑だったり間違いのもとだったりするからかヘルパ関数(std::move)が用意されています.

	// move
	// int & lr3 = std::move(x); // ERROR
	int && rr3 = std::move(x); // xrvalue

std::moveは単純に引数をrvalue referenceにキャストして返してくれます.
このようなキャストによるものをxvalueと呼ぶようです.
rvalueはprvalueとxvalueを総称した呼び方ということになります.

lvalue referenceとrvalue referenceは型として区別されるので,オーバーロードすることができます.
特に,コピーコンストラクタとコピー代入演算子に関してその引数であるconst lvalue referenceをrvalue referenceに置き換えたムーブコンストラクタ・ムーブ代入演算子の概念ができました.

struct Bar
{
	Bar(){ std::cout << "default ctor" << std::endl; } 
	Bar(Bar const&){ std::cout << "copy ctor" << std::endl; }
	Bar(Bar&&){ std::cout << "move ctor" << std::endl; }
	Bar&operator=(Bar const&){ std::cout << "copy subst" << std::endl; return *this; }
	Bar&operator=(Bar&&){ std::cout << "move subst" << std::endl; return *this; }
};
<||
>|cpp|
	std::cout << "1: "; Bar b1{};
	std::cout << "2: "; Bar b2(b1);
	std::cout << "3: "; Bar b3(Bar{});
	std::cout << "4: "; Bar b4(std::move(b3));

	std::cout << "5: "; b1 = b2;
	std::cout << "6: "; b1 = Bar{};
	std::cout << "7: "; b1 = std::move(b4);

output

1: default ctor
2: copy ctor
3: default ctor
4: move ctor
5: copy subst
6: default ctor
move subst
7: move subst

出力を入れるために自分で定義していますが,コンストラクタ・代入演算子を記述しなければmove二つを含めたこれらが暗黙に定義されます.
引数となる値に合わせてcopy,moveが呼び分けられています.

3の挙動は正確に理解していないのですが,この書き方でもただデフォルトコンストラクタが呼ばれています.
結果的にこれが正しいのはわかりますが,最適化を外して副作用を持ったコンストラクタにしてみても挙動は変わりませんでした.
こんな書き方を態々することはなかなかないと思いますが,場合によっては少し注意が必要かもしれません.

2013-06-05追記

3については恐らくInitialization of class objects by rvaluesによる最適化が働くことで
b3の領域で直接構築が行われた,と解釈すれば良いのでしょう.


もちろん,これらの特殊メンバ関数だけでなく一般の関数やメンバ関数にもこれらの型によるオーバーロードは有効です.

struct Baz
{
	void f(Foo const&){ std::cout << "lvalue ref" << std::endl; }
	void f(Foo&&) { std::cout << "rvalue ref" << std::endl; }
	...
};
	Foo foo;
	Baz baz;
	baz.f(foo);
	baz.f(Foo{});

output

lvalue ref
rvalue ref

注意点としては,rvalue reference型の変数自体はlvalueとして扱わざるを得ないのでそれをrvalueとして渡したい場合は再度キャストが必要であるということが挙げられます.

また,テンプレートの中で,rvalue referenceであればムーブしそうでなければコピーしたいといったケースが現れることがあります.
この挙動をPerfect Forwarding(完全転送)と言い,std::forwardに渡すことでこれを実現できます.

基本的にはユーザ側が意識しなくともライブラリの実装がこれらを活用したものに変わっていくことで間接的に恩恵の受けられる機能かと思います.
重たいオブジェクトを扱う際にはこれを活用することで,効率的でスマートな動作を実現できるでしょう.

*1:前述のconst lvalue referenceを除く