rvalueリファレンス
概要
いままで使っているリファレンスは、正式にはlvalueリファレンスという名前がついている。これはlvalueへのリファレンスという意味だ。lvalueへのリファレンスがあるからには、lvalueではないリファレンスがあるということだ。C++にはrvalueへのリファレンスがある。これをrvalueリファレンスという。
この章で説明する内容はとても難しい。完全に理解するためには、何度も読み直す必要があるだろう。
rvalueリファレンスの宣言
T型へのlvalue型リファレンス型はT &と書く。
T & lvalue_reference = ... ;T型へのrvalueリファレンス型はT &&と書く。
T && rvalue_reference = ... ;lvalueリファレンスはlvalueで初期化する。rvalueリファレンスはrvalueで初期化する。
lvalueとは名前付きのオブジェクトや戻り値の型としてのlvalueリファレンスのことだ。
int object { } ;
int & f() { return object ; }
int main()
{
// lvalueリファレンス
int & a = object ;
int & b = f() ;
}ここで、式objectや式f()を評価した結果はlvalueだ。
rvalueとは、名前なしのオブジェクトや計算結果の一時オブジェクト、戻り値の型としてのrvalueリファレンスのことだ。
int && g() { return 0 ; }
int h() { return 0 ; }
int main()
{
// rvalueリファレンス
int && a = 0 ;
int && b = 1 + 1 ;
int && c = g() ;
int && d = h() ;
}ここで、式0、式1 + 1、式g()を評価した結果はrvalueだ。
rvalueリファレンスをlvalueで初期化することはできない。
int object { } ;
int & f() { return object ; }
int main()
{
// すべてエラー
int && a = object ;
int && b = f() ;
}lvalueリファレンスをrvalueで初期化することはできない。
int && g() { return 0 ; }
int h() { return 0 ; }
int main()
{
// すべてエラー
int & a = 0 ;
int & b = 1 + 1 ;
int & c = g() ;
int & d = h() ;
}リファレンスを初期化することを、リファレンスはリファレンス先を束縛するという。lvalueリファレンスはlvalueを束縛する。rvalueリファレンスはrvalueを束縛する。
ただし、constなlvalueリファレンスはrvalueを束縛することができる。
int && g() { return 0 ; }
int main()
{
// OK、constなlvalueリファレンス
const int & a = 0 ;
const int & b = 1 + 1 ;
const int & c = g() ;
}rvalueリファレンス自体はlvalueだ。なぜならばrvalueリファレンスはオブジェクトに名前を付けて束縛するからだ。
int main()
{
// rvalueリファレンス
int && a = 0 ;
// OK、rvalueリファレンスaはlvalue
int & b = a ;
// エラー、rvalueリファレンスaはrvalueではない
int && b = a ;
}値カテゴリー
lvalueとrvalueとは何か。もともとlvalueとは左辺値(left-hand value)、rvalueとは右辺値(right-hand value)という語源を持っている。これはまだC言語すらなかったはるか昔から存在する用語で、代入式の左辺に書くことができる値をlvalue、右辺に書くことができる値をrvalueと読んでいたことに由来する。
lvalue = rvalue ;例えば、int型の変数xは代入式の左辺に書くことができるからlvalue、整数リテラル0は右辺に書くことができるからrvalueといった具合だ。
int x ;
x = 0 ;C++ではlvalueとrvalueをこのような意味では使っていない。
lvalueとrvalueを理解するには、値カテゴリーを理解しなければならない。
- 式(expression)とは
glvalueかrvalueである。 glvalueとはlvalueかxvalueである。rvalueとはprvalueかxvalueである。
この関係を図示すると以下のようになる。
TODO: 図示
expression
/ \
glvalue rvalue
/ \ / \
lvalue xvalue prvaluefig/fig37-01.png
lvalue
lvalueはすでに説明したとおり名前付きのオブジェクトのことだ。
// lvalue
int object ;
int & ref = object ;通常使うほとんどのオブジェクトはlvalueになる。
prvalue
prvalueは純粋なrvalue(pure rvalue)のことだ。つまり、名前なしのオブジェクトや計算結果の一時オブジェクトのことだ。
int f() { return 0 ; }
// prvalue
0 ;
1 + 1 ;
f() ;ほとんどのprvalueは式を評価するときに自動的に生成され、自動的に破棄されるので、あまり意識することはない。
関数の戻り値の型がリファレンスではない場合、一時オブジェクトが生成される。
struct X { } ;
X f() ;演算子も関数の一種なので、
auto result = x + y + z ;のような式がある場合、まずx + yが評価され、その結果が一時オブジェクトとして返される。その一時オブジェクトを仮にtempとすると、temp + zが評価され、また一時オブジェクトが生成され、変数resultに代入される。
式文全体を評価し終わったあとに、一時オブジェクトは自動的に破棄される。
一時オブジェクトは自動的に生成され、自動的に破棄される。ここがとても重要な点だ。これは次の章で説明するムーブセマンティクスに関わってくる。
xvalue
xvalueとは寿命が尽きかけているlvalue(eXpiring lvalue)のことだ。xvalueはlvalueやprvalueから変換することで発生する。
xvalueとなる値は以下のような場合だ。
- 戻り値の型がオブジェクトの型への
rvalueリファレンスである関数の呼び出しの結果
int && f() { return 0 ; }
int main()
{
// xvalue
int && r = f() ;
}- オブジェクトの型への
rvalueリファレンスへのキャスト
int main()
{
int object{} ;
// xvalue
int && r = static_cast<int &&>(object) ;
}xvalue配列への添字操作
int main()
{
int a[3] = {1,2,3} ;
int && r = static_cast<int (&&)[3]>(a)[0] ;
}xvalue配列というのは配列のオブジェクトを配列へのrvalueリファレンス型にキャストすると得られる。xvalue配列への添字操作の結果はxvalueだ。
xvalueなクラスのオブジェクトへのリファレンスではない非staticデータメンバーへのアクセス
struct X { int data_member ; } ;
int main()
{
X x{} ;
int && r = static_cast<X &&>(x).data_member ;
}- 式
.*で最初のオペランドがxvalueで次のオペランドがデータメンバーへのポインターの場合
struct X { int data_member ; } ;
int main()
{
X x{} ;
int && r = static_cast<X &&>(x).*&X::data_member ;
}これも配列と似ていて、xvalueのクラスオブジェクトに対するメンバーへのポインター経由でのメンバーの参照結果はxvalueになるということだ。
重要なのは最初の2つだ。残りは覚える必要はない。重要なのは、xvalueとは、lvalueかprvalueから変換した結果発生するものだ。
rvalue
prvalueとxvalueを合わせて、rvalueという。rvalueリファレンスというのは、rvalueでしか初期化できない。rvalueというのはprvalueかxvalueのどちらかだ。
lvalueはxvalueに変換できるので、結果としてrvalueに変換できることになる。
int main()
{
// lvalueなオブジェクト
int lvalue { } ;
// OK、lvalueリファレンスはlvalueで初期化できる
int & l_ref = lvalue ;
// OK、rvalueリファレンスはrvalueで初期化できる
// rvalueリファレンスにキャストした結果はrvalue
int && r_ref = static_cast<int &&>(lvalue) ;
}lvalueはそのままではrvalueではないが、xvalueに変換すればrvalueになる。
prvalueはもともとrvalueである。
この性質は次の章で説明するムーブセマンティクスで利用する。
glvalue
glvalueは一般的なlvalue(generalized lvalue)という意味だ。glvalueとは、lvalueかxvalueのことだ。
lvalueから変換したxvalueはもともとlvalueだったのだから、glvalueとなるのも自然だ。xvalueに変換したprvalueはglvalueになれる。
この性質はムーブセマンティクスで利用する。
rvalueリファレンスのライブラリ
std::move
std::move(e)は値eをxvalueにするための標準ライブラリだ。std::move(e)は値eの型Tへのrvalueリファレンス型にキャストしてくれるので、xvalueになる。そしてxvalueはrvalueだ。
int main()
{
int lvalue { } ;
int && r = std::move(lvalue) ;
}これは以下のように書いたものと同じようになる。
int main()
{
int lvalue { } ;
int && r = static_cast<int &&>(lvalue) ;
}std::moveの実装
std:move(e)の実装は少し難しい。根本的には、式eのリファレンスではない型Tに対して、static_cast<T &&>(e)をしているだけだ。
すると以下のような実装だろうか。
template < typename T >
T && move( T & t ) noexcept
{
return static_cast<T &&>(t) ;
}この実装はlvalueをxvalueに変換することはできるが、rvalue(prvalueとxvalue)をxvalueに変換することはできない。
int main()
{
// エラー、prvalueを変換できない
int && r1 = move(0) ;
int lvalue { } ;
// エラー、xvalueをxvalueに変換できない
int && r2 = move(move(lvalue)) ;
}rvalueはrvalueリファレンスで受け取れるので、lvalueリファレンスを関数の引数として受け取るmoveのほかに、rvalueリファレンスを関数の引数として受け取るmoveを書くとよい。
すると以下のように書けるだろうか。
// lvalueリファレンス
template < typename T >
T && move( T & t ) noexcept
{
return static_cast<T &&>(t) ;
}
// rvalueリファレンス
template < typename T >
T && move( T && t ) noexcept
{
return static_cast<T &&>(t) ;
}しかしこれでは関数の本体の中身がまったく同じ関数が2つできてしまう。もっと複雑な関数を書くときにこのようなコードの重複があると、ソースコードの修正が難しくなる。せっかくテンプレートを使っているのにこれでは意味がない。
フォワーディングリファレンス
C++のテンプレートはコードの重複を省くためにある。そのため、C++ではテンプレートパラメーターへのrvalueリファレンスを関数の仮引数として取る場合を、フォワーディングリファレンス(forwarding reference)として、特別にlvalueでもrvalueでも受け取れるようにしている。
// T &&はフォワーディングリファレンス
template < typename T >
void f( T && t ) ;このような関数テンプレートの仮引数tに実引数としてrvalueを渡すと、Tはrvalueの型となり、結果としてtの型はT &&になる。
// Tはint
f(0) ;もし実引数として型Uのlvalueを渡すと、テンプレートパラメーターTがU &となる。そして、テンプレートパラメーターTに対するリファレンス宣言子(&, &&)は単に無視される。
int lvalue{} ;
// Tはint &
// T &&はint &
f(lvalue) ;ここで、関数テンプレートfのテンプレートパラメーターTはint &となる。このTにリファレンス宣言子をT &やT &&のように使っても、単に無視されて、T &となる。
template < typename T >
void f( T && t )
{
using A = T & ;
using B = T && ;
}
int main()
{
// prvalue
f(0) ;
int lvalue{} ;
// lvalue
f(lvalue) ;
}f(0)はprvalueを渡している。この場合、Tの型はintとなる。Aはint &、Bはint &&となる。
f(lvalue)はlvalueを渡している。この場合、Tの型はint &となる。この場合のTに&や&&を付けても無視される。なので、A, Bの型はどちらもint &になる。
したがって、以下のように書くとmoveはlvalueもrvalueも受け取ることができる。
// lvalueもrvalueも受け取ることができるmove
template < typename T >
T && move( T && t ) noexcept
{
return static_cast<T &&>(t) ;
}ただし、この実装にはまだ問題がある。このmoveにlvalueを渡した場合、lvalueの型をUとすると、テンプレートパラメーターTはU &になる。
U lvalue{} ;
// TはU &
move( lvalue ) ;テンプレートパラメーター名Tがリファレンスのとき、Tにリファレンス宣言子&&を付けても単に無視されることを考えると、上のmoveにint &型のlvalueが実引数として渡されたときは、以下のように書いたものと等しくなる。
int & move( int & t ) noexcept
{
return static_cast<int &>(t) ;
}move(e)はeがlvalueであれrvalueであれ、xvalueにする関数だ。そのためには、rvalueリファレンスにキャストしなければならない。テンプレートではフォーワーディングリファレンスという例外的な仕組みによってlvalueもrvalueもT &&で受け取れるが、lvalueを受け取ったときにはT &&がlvalueリファレンスになってしまうのでは、xvalueにキャストできない。
この問題は別のライブラリによって解決できる。
std::remove_reference_t/
std::remove_reference_t<T>はT型からリファレンス型を除去してくれるライブラリだ。
int main()
{
// int
using A = std::remove_reference_t<int> ;
// int
using B = std::remove_reference_t<int &> ;
// int
using C = std::remove_reference_t<int &&> ;
}ということは、これとリファレンス宣言子を組み合わせると、どのような型がテンプレート実引数に渡されてもrvalueリファレンスにできる。
template < typename T >
void f()
{
using RT = std::remove_reference_t<T> && ;
}add_pointer_t/remove_pointer_tがあるように、remove_reference_tにも対となるリファレンスを追加するライブラリが存在する。ただしリファレンスにはlvalueリファレンスとrvalueリファレンスがあるので、それぞれstd::add_lvalue_reference_t<T>、std::add_rvalue_reference_t<T>となっている。
int main()
{
// int &
using A = std::add_lvalue_reference_t<int> ;
// int &&
using B = std::add_rvalue_reference_t<int> ;
}std::moveの正しい実装
std::remove_reference_t<T>を使うと、moveは以下のように書ける。
template < typename T >
std::remove_reference_t<T> && move( T && t ) noexcept
{
return static_cast< std::remove_reference_t<T> && >(t) ;
}std::forward
テンプレートパラメーターにrvalueリファレンス宣言子を使うとlvalueもrvalueも受け取れる。
template < typename T >
void f( T && t ) { }
int main()
{
int lvalue{} ;
f(lvalue) ;
f(0) ;
}この関数fから別の関数gに値を渡したい場合を考えよう。
template < typename T >
void g( T && t ) { }
template < typename T >
void f( T && t )
{
g(t) ;
}このとき、関数fに渡されたものがlvalueでもrvalueでも、関数gに渡される値はlvalueになってしまう。
なぜならば、名前付きのrvalueリファレンスに束縛されたオブジェクトはlvalueだからだ。
int main()
{
// 名前付きのrvalueリファレンス
int && rvalue_ref = 0 ;
// これはlvalue
int & lvalue_ref = rvalue_ref ;
}なので、g(t)のtはlvalueとなる。
ここでrvalueを渡すのは簡単だ。std::moveを使えばいい。
template < typename T >
void f( T && t )
{
g( std::move(t) ) ;
}ただし、これはtがlvalueのときも問答無用でxvalueにしてしまう。
tがlvalueならばlvalueとして、rvalueならばxvalueとして、渡された値カテゴリーのまま別の関数に渡したい場合、std::forward<T>(t)が使える。
template < typename T >
void f( T && t )
{
g( std::forward<T>(t) ) ;
}std::forward<T>(t)のTにはテンプレートパラメーター名を書く。こうすると、tがlvalueならばlvalueリファレンス、rvalueならばrvalueリファレンスが戻り値として返される。
std::forwardの実装は以下のとおりだ。
template<class T>
constexpr
T &&
forward(remove_reference_t<T>& t) noexcept
{ return static_cast<T&&>(t) ; }
template<class T>
constexpr
T &&
forward(remove_reference_t<T>&& t) noexcept
{ return static_cast<T&&>(t) ; }もしstd::forward<T>(t)にlvalueが渡された場合、上のforwardが呼ばれる。その場合、Tはlvalueリファレンスになっているはずなのでrvalueリファレンス宣言子は無視され、lvalueリファレンスが戻り値の型になる。
rvalueが渡された場合、rvalueリファレンスが戻り値の型になる。