Skip to content

Latest commit

 

History

History
603 lines (440 loc) · 17.7 KB

037-rvalue-reference.md

File metadata and controls

603 lines (440 loc) · 17.7 KB

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を束縛する。

ただし、constlvalueリファレンスは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 ;
}

値カテゴリー

lvaluervalueとは何か。もともとlvalueとは左辺値(left-hand value)、rvalueとは右辺値(right-hand value)という語源を持っている。これはまだC言語すらなかったはるか昔から存在する用語で、代入式の左辺に書くことができる値をlvalue、右辺に書くことができる値をrvalueと読んでいたことに由来する。

lvalue = rvalue ;

例えば、int型の変数xは代入式の左辺に書くことができるからlvalue、整数リテラル0は右辺に書くことができるからrvalueといった具合だ。

int x ;
x = 0 ;

C++ではlvaluervalueをこのような意味では使っていない。

lvaluervalueを理解するには、値カテゴリーを理解しなければならない。

  1. 式(expression)とはglvaluervalueである。
  2. glvalueとはlvaluexvalueである。
  3. rvalueとはprvaluexvalueである。

この関係を図示すると以下のようになる。

TODO: 図示

        expression
        /         \
    glvalue     rvalue
    /     \     /   \
lvalue     xvalue   prvalue

fig/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)のことだ。xvaluelvalueprvalueから変換することで発生する。

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とは、lvalueprvalueから変換した結果発生するものだ。

rvalue

prvaluexvalueを合わせて、rvalueという。rvalueリファレンスというのは、rvalueでしか初期化できない。rvalueというのはprvaluexvalueのどちらかだ。

lvaluexvalueに変換できるので、結果として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とは、lvaluexvalueのことだ。

lvalueから変換したxvalueはもともとlvalueだったのだから、glvalueとなるのも自然だ。xvalueに変換したprvalueglvalueになれる。

この性質はムーブセマンティクスで利用する。

rvalueリファレンスのライブラリ

std::move

std::move(e)は値exvalueにするための標準ライブラリだ。std::move(e)は値eの型Tへのrvalueリファレンス型にキャストしてくれるので、xvalueになる。そしてxvaluervalueだ。

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) ;
}

この実装はlvaluexvalueに変換することはできるが、rvalue(prvaluexvalue)をxvalueに変換することはできない。

int main()
{
    // エラー、prvalueを変換できない
    int && r1 = move(0) ;

    int lvalue { } ;
    // エラー、xvalueをxvalueに変換できない
    int && r2 = move(move(lvalue)) ;
}

rvaluervalueリファレンスで受け取れるので、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を渡すと、Trvalueの型となり、結果としてtの型はT &&になる。

// Tはint
f(0) ;

もし実引数として型Ulvalueを渡すと、テンプレートパラメーターTU &となる。そして、テンプレートパラメーターTに対するリファレンス宣言子(&, &&)は単に無視される。

int lvalue{} ;
// Tはint &
// T &&はint &
f(lvalue) ;

ここで、関数テンプレートfのテンプレートパラメーターTint &となる。この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となる。Aint &Bint &&となる。

f(lvalue)lvalueを渡している。この場合、Tの型はint &となる。この場合のT&&&を付けても無視される。なので、A, Bの型はどちらもint &になる。

したがって、以下のように書くとmovelvaluervalueも受け取ることができる。

// lvalueもrvalueも受け取ることができるmove
template < typename T >
T && move( T && t ) noexcept
{
    return static_cast<T &&>(t) ;
}

ただし、この実装にはまだ問題がある。このmovelvalueを渡した場合、lvalueの型をUとすると、テンプレートパラメーターTU &になる。

U lvalue{} ;
// TはU &
move( lvalue ) ;

テンプレートパラメーター名Tがリファレンスのとき、Tにリファレンス宣言子&&を付けても単に無視されることを考えると、上のmoveint &型のlvalueが実引数として渡されたときは、以下のように書いたものと等しくなる。

int & move( int & t ) noexcept
{
    return static_cast<int &>(t) ;
}

move(e)elvalueであれrvalueであれ、xvalueにする関数だ。そのためには、rvalueリファレンスにキャストしなければならない。テンプレートではフォーワーディングリファレンスという例外的な仕組みによってlvaluervalueT &&で受け取れるが、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リファレンス宣言子を使うとlvaluervalueも受け取れる。

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)tlvalueとなる。

ここでrvalueを渡すのは簡単だ。std::moveを使えばいい。

template < typename T >
void f( T && t )
{
    g( std::move(t) ) ;
}

ただし、これはtlvalueのときも問答無用でxvalueにしてしまう。

tlvalueならばlvalueとして、rvalueならばxvalueとして、渡された値カテゴリーのまま別の関数に渡したい場合、std::forward<T>(t)が使える。

template < typename T >
void f( T && t )
{
    g( std::forward<T>(t) ) ;
}

std::forward<T>(t)Tにはテンプレートパラメーター名を書く。こうすると、tlvalueならば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が呼ばれる。その場合、Tlvalueリファレンスになっているはずなのでrvalueリファレンス宣言子は無視され、lvalueリファレンスが戻り値の型になる。

rvalueが渡された場合、rvalueリファレンスが戻り値の型になる。