2011-04-12

Perfect Fowardingとは何か

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: