C++ 11 の rvalueリファレンスと reference collapsing

rvalueリファレンス

rvalueリファレンス自体については「C++ 11 の rvalueリファレンス」に譲ります

reference collapsing

C++ 11 からリファレンスのリファレンスが解禁されました
&記号を3つも4つも書けるようになったというわけではありませんが、typedef やテンプレート引数などが参照型でも気にせず参照型を宣言できるようになりました

template<typename T> void foo( T& );  // T が int& などの参照型でもエラーにならなくなった
int&&& rrr; // こんな宣言はできない

標準では次のように言っています

N3797
8 Declarators
8.3 Meaning of declarators
8.3.2 References
6 If a typedef-name (7.1.3, 14.1) or a decltype-specifier (7.1.6.2) denotes a type TR that is a reference to a type T, an attempt to create the type "lvalue reference to cv TR" creates the type "lvalue reference to T", while an attempt to create the type "rvalue reference to cv TR" creates the type TR.
[ Example:
int i;
typedef int& LRI;
typedef int&& RRI;
LRI& r1 = i; // r1 has the type int&
const LRI& r2 = i; // r2 has the type int&
const LRI&& r3 = i; // r3 has the type int&
RRI& r4 = i; // r4 has the type int&
RRI&& r5 = 5; // r5 has the type int&&
decltype(r2)& r6 = i; // r6 has the type int&
decltype(r2)&& r7 = i; // r7 has the type int&
- end example ]

これは要するに rvalue参照よりも lvalue参照を優先するということです

T&  + &  → T&
T&& + &  → T&
T&  + && → T&
T&& + && → T&&

この新しいルールを reference collapsing といいます
なんでもない決まりなのですが、これがちょっとした問題を起こす場合があります

template と rvalueリファレンス

次のテンプレート関数は引数に rvalue参照をとると宣言しています
rvalue参照を受け取るのですから実装は当然それを破壊します

template<typename T> void foo( T&& t ){
    // t は rvalue参照だから壊すよ
}

reference collapsing が無いのであればそれで構わないのですが、実際には reference collapsing があります
T が CFoo& だった場合、T&& は CFoo& + && ですから CFoo& であるとみなされることになります
この場合は CFoo& は lvalue参照ですから壊してはいけません

int aaa = 0;
int& bbb = aaa;
int&& ccc = 10;
int& ddd = 100; // 非constな lvalue参照が rvalue を参照することはできない
int&& eee = aaa; // rvalue参照が lvalue を参照することはできない
CFoo fff;
foo( fff ); // rvalue を期待しているところに lvalue を渡しているのにエラーにならない→ 壊される!

困りますね
困るのでテンプレート関数に rvalue参照をとる引数を持たせるときは必ず lvalue参照用の実装を用意します
必要なければ空の宣言にします

template<typename T> void foo( T& t ) = delete;

reference collapsing を回避する

テンプレート関数に rvalue参照をとる引数を持たせるときは必ず lvalue参照用の実装を用意します。と言いましたが嘘です

嘘というか別にそうしても構わないのですが、そんなことをしようとすると rvalue参照引数の数の組み合わせの分だけ別々の実装を用意する必要があります
rvalue参照引数が一つだけであれば一つ余計に用意すればいいだけですが、2つになっただけでも3つの余計な実装を用意しなければいけません

template<typename T> void foo( T&& t1, T&& t2 );
template<typename T> void foo( T& t1, T&& t2 ) = delete;
template<typename T> void foo( T&& t1, T& t2 ) = delete;
template<typename T> void foo( T& t1, T& t2 ) = delete;

rvalue参照引数が3つなら7つも余計に用意しなければいけません

template<typename T> void foo( T&& t1, T&& t2, T&& t3 );
template<typename T> void foo( T& t1, T&& t2, T&& t3 ) = delete;
template<typename T> void foo( T&& t1, T& t2, T&& t3 ) = delete;
template<typename T> void foo( T& t1, T& t2, T&& t3 ) = delete;
template<typename T> void foo( T&& t1, T&& t2, T& t3 ) = delete;
template<typename T> void foo( T& t1, T&& t2, T& t3 ) = delete;
template<typename T> void foo( T&& t1, T& t2, T& t3 ) = delete;
template<typename T> void foo( T& t1, T& t2, T& t3 ) = delete;

用意し忘れや間違いの元になって危険ですし、何より面倒ですね
誰もこんなものは書きたくないでしょう
どうしたらいいでしょうか

そもそも悪いのは reference collapsing です
reference collapsing が T&& を T& にしてしまうからそれを区別するために余計な空宣言を書かなくてはいけなくなってしまっているのです
なぜ reference collapsing なんてものがあるのでしょうか

reference collapsing を利用する

なぜ reference collapsing なんてものがあるのかというと正にこのように大量の実装を用意しなくてもいいようにあるのです

今苦労しているのは reference collapsing がせっかくまとめてくれたものをそれに逆らって区別しようとしているからなのです
reference collapsing に逆らうのではなくうまく利用してあげると楽になります

そもそもの問題は rvalue参照だと思っているところに壊してはいけない lvalue が渡ってきてしまうと困るということでした
引数の型が rvalue参照だったらそのように扱い、lvalue参照だったらそのように扱う。「型が○○だったら」といえばテンプレートです

reference collapsing がまとめてくれたものを無理に区別するのではなくそこだけ別のテンプレートに追い出して場合分けをすればいいのです
そうすれば実装は一つにまとまっていながら rvalue参照にも lvalue参照にも対応した実装を書くことができます
処理を分けなければいけないのは同じですが、丸ごと同じようなコードを無駄にいくつも書くよりは必要なところを必要なだけ書く方がずっといいでしょう

次の実装を一つにまとめてみましょう

template<typename T,typename U> void	my_copy( T&	d, U&&	s ){
	d = std::move(s);
}
template<typename T,typename U> void	my_copy( T&	d, U&	s ){
	d = s;
}

ここでやりたいのは std::move を呼ぶかどうかということではありません
ここでやりたいのは U&& が rvalue参照のときは d のムーブ代入演算子を呼び出し、U&& が lvalue参照のときは d のコピー代入演算子を呼び出すということです
静的な場合分けはそこを別のテンプレートに分けるのでしたが、ここはもっと簡単です

逆に考えてみましょう
ムーブ代入演算子を呼び出すには operator= の引数に rvalue参照を渡せばよく、コピー代入演算子を呼び出すには operator= の引数に lvalue参照を渡せばよい
これはつまり U&& そのものですね

ということは s の型は U&& ですからもしかして何もせずただ代入するだけでいいのでしょうか

template<typename T, typename U> void	my_copy( T&	d, U&&	s ){
	d = s;
}

しかしこれは常にコピー代入演算子が呼ばれてしまいます
なぜでしょうか
それは s が rvalue参照であるときも s 自身は lvalue であるからです
s には s という名前が付いていますね
名前が付いているものは lvalue です
ですから s の型が rvalue参照であっても lvalue参照であっても s 自身は常に lvalue なのです
operator= に lvalue を渡せば呼ばれるのはコピー代入演算子です

では std::move を呼べばいいのでしょうか
しかし std::move は常に rvalue参照を返すのですから当たり前ですが、それでは今度は常にムーブ代入演算子が呼ばれるようになってしまいます

template<typename T, typename U> void	my_copy( T&	d, U&&	s ){
	d = std::move(s);
}

困りましたね
U&& 自体は望みのものです
しかしそれを使おうとすると思った通りになりません
どうすればいいのでしょうか

実はこれにはとても簡単な解決策があります
それは明示的に渡すものが U&& であると指定すればいいのです
つまりここは U&& へ static_cast してあげるというのが正解です

template<typename T, typename U> void	my_copy( T&	d, U&&	s ){
	d = static_cast<U&&>(s);
}

こうすると s が rvalue参照の時は operator= に rvalue参照を渡し、s が lvalue参照の時は operator= に lvalue参照を渡すという処理になります

std::forward

この渡されたものの型が rvalue参照の時は rvalue参照を返し、渡されたものの型が lvalue参照の時は lvalue参照を返すという処理は一般的なので標準ではこれに std::forward という名前を付けました
やっていることは上記の通りただの static_cast です

std::forward と std::move

std::forward のやっていることはただの static_cast です
std::move もやっていることはただの static_cast でした
何が違うのでしょう

std::move は渡されたものが rvalue参照でも lvalue参照でも常に rvalue参照を返すというものです

namespace std {
template<typename T> inline typename remove_reference<T>::type&& move( T&& t ){
	return	static_cast<remove_reference<T>::type&&>(t);
}
}

std::forward がやっているのは渡されたものが rvalue参照の時は rvalue参照を返し、lvalue参照のときは lvalue参照を返すというものです

namespace std {
template<typename T> inline T&&	forward( typename remove_reference<T>::type&&	t ){
	return	static_cast<T&&>(t);
}
template<typename T> inline T&&	forward( typename remove_reference<T>::type&	t ){
	return	static_cast<T&&>(t);
}
}

似たようなことですがこれを言い換えると、lvalue参照と rvalue参照をまとめるのが std::move で、lvalue参照と rvalue参照を分けるのが std::forward です
つまり仕事が逆なのです

気を付けよう

それが rvalue であるということと、それが rvalue参照であるということは同じではないということに気を付けてください
そして template で rvalue参照を使うときは reference collapsing が働くのでうっかり無条件に壊してしまわないように気を付けましょう

というか、うっかりミスをしてしまう余地があるなら reference collapsing を期待することを明示して T&&& と書くとか T&&? と書くとか T&+ と書くとかって言語仕様にすればいいだけなのに