Perfect Fowardingとは、関数テンプレートの引数としてのリファレンスを、別の関数に渡す際のテクニックである。ふたつの問題がある。
名前のついたrvalueリファレンスの変数はlvalueである。
int && r1 = 0 ; int && r2 = r1 ; // エラー、r1はlvalue
これは当然である。なぜならば、rvalueリファレンスは、rvalueを束縛して、lvalueのように使うリファレンスである。リファレンスが勝手に、rvalueとして認識されてしまっては困る。もし、rvalueとして使いたい場合は、明示的なキャストが必要である。
int && r1 = 0 ; int && r2 = std::move( r1 ) ; // OK
また、以下の様な形の関数テンプレートを宣言するとき、
template < typename T > void f( T && ref ) ;
refはlvalueリファレンスにもrvalueリファレンスにもインスタンス化される。
template < typename T > void f( T && ref ) { } int main() { int l = 0 ; // lvalue int && r = 0 ; // rvalue f( l ) ; // Tはint &型、&&は無視される、refの型はint &型 f( r ) ; // Tはint型、refの型はint &&型 }
これは一見奇妙だが、そういうルールになっている。このルールによって、ひとつのテンプレートで、lvalueとrvalueの両方に対応できる。
しかし問題は、この関数fから、さらにrvalue referenceを他の関数に渡したい場合だ。
void g( int & ) ; // #1 void g( int && ) ; // #2 template < typename T > void f( T && ref ) { g( ref ) ; // 常に#1を呼ぶ }
このコードは、rvalueを正しくforwardしない。常に#1を呼ぶ。なぜならば、たとえrefの型がrvalueリファレンスだったとしても、ref自体はlvalueだからだ。
何故こうなっているのかというと、関数fは、関数gを呼び出した後に、refを使うかもしれない。その際、refがrvalueとして関数gに渡されていては、ムーブされてしまうかもしれない。この意図せぬムーブを防ぐために、rvalueリファレンスはlvalueなのだ。
では、rvalueとして渡したい場合は、std::moveを使えばいいのか。実は、それでは問題がある。関数にlvalueが渡されたときにも、ムーブしてしまうからだ。
template < typename T > void f( T && ref ) { g( std::move(ref) ) ; } class X { /* 実装 */ } ; int main() { X x ; f( x ) ; // lvalueとして渡す }
ここで必要なのは、refがlvalueリファレンスの場合はlvalueで、rvalueリファレンスの場合はrvalueで、関数gに渡すことである。このために、std::forwardがある。
template < typename T > void f( T && ref ) { g( std::forward<T>(ref) ) ; // 常に#1を呼ぶ }
このように書くだけで、std::forwardは、lvalueのときにはlvalueで、rvalueのときにはrvalueとして、魔法のようにパーフェクトなforwardingをしてくれる。
No comments:
Post a Comment