いままで使っているリファレンスは、正式にはlvalue
リファレンスという名前がついている。これはlvalue
へのリファレンスという意味だ。lvalue
へのリファレンスがあるからには、lvalue
ではないリファレンスがあるということだ。C++には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 prvalue
fig/fig37-01.png
lvalue
はすでに説明したとおり名前付きのオブジェクトのことだ。
// lvalue
int object ;
int & ref = object ;
通常使うほとんどのオブジェクトはlvalue
になる。
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
とは寿命が尽きかけている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
から変換した結果発生するものだ。
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
は一般的なlvalue
(generalized lvalue)という意味だ。glvalue
とは、lvalue
かxvalue
のことだ。
lvalue
から変換したxvalue
はもともとlvalue
だったのだから、glvalue
となるのも自然だ。xvalue
に変換したprvalue
はglvalue
になれる。
この性質はムーブセマンティクスで利用する。
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(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<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::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) ;
}
テンプレートパラメーターに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
リファレンスが戻り値の型になる。