右辺値参照とムーブ・セマンティクス

公開:2013-03-25 13:33
更新:2020-02-15 04:37
カテゴリ:c++

右辺値参照に関することがいまだに腑に落ちていない。いままで読んだブログ記事を引用してまとめると理解が進むような気もするのでやってみた。

左辺値(lvalue)・右辺値(rvalue)とは何か

lvalueとは、明示的に実体のある、名前付きのオブジェクトであり、rvalueとは、一時的に生成される無名のオブジェクトである。

struct X{} ;
int f() { return 0 ; }

int main()
{
  int i = 0 ;

  i ;   // lvalue
  0 ;   // rvalue

  X x ;

  x ;   // lvalue
  X() ;  // rvalue

  f() ;  // rvalue
}
http://cpplover.blogspot.jp/2009/11/rvalue-reference_23.html

そもそも左辺値とは、およそ(constでなければ)代入演算子の左辺に置けるオブジェクトです。一方、右辺値は代入演算子の右辺に置けるものということで、一時オブジェクト(関数の戻り値もそう)、数値リテラルなどがあります(ただし当然ながら左辺値も右辺値として扱えます)。

http://dev.activebasic.com/egtra/2007/02/15/36/

C++では,すべての式はlvalue(locator value)またはrvalueのどちらかである. 平易に説明すれば, lvalueとは代入の左辺,つまり代入先に指定できるような式,rvalueとは代入先に指定できない式という感じ. これだけ聞くと“要するに変数?”と思いたくなるかもしれないけど, C++ではリファレンスを返す関数がつくれたり,演算子(キャスト演算子含む)がオーバーロードできたりするので,そんなに単純でもなかったりする.

http://marigold.sakura.ne.jp/devel/cpp/lvalue.html

lvalue/rvalueは,もともとK&Rでは“left value”(左辺値),“right value”(右辺値)の意味だった. この左辺値・右辺値という表現は直感的でわかりやすいのだけども,よく考えてみると何をして左辺値・右辺値というかが意外にわかりにくい. そこでANSI規格になった時点では,少し抽象的な表現“Locator VALUE”(位置を特定する値)の意味と読み替えたという経緯があるようだ (ただし,規格では“Locator VALUE”という表現はださず,そのまま“lvalue”という言語要素としている).

http://marigold.sakura.ne.jp/devel/cpp/lvalue.html

左辺値・右辺値というのはC言語でのみ通用する言葉で、誤解を生みやすい。右辺値とは無名一時オブジェクトもしくはリテラル、左辺値とは名前付きオブジェクトのことだと考えるとよいのだろうか。

右辺値参照とは何か

rvalue referenceとは、その名の通り、rvalueに対する参照である。文章で説明するより、コードを示した方が分かりやすい。

struct X{ } ;

int main()
{
  X x ;

  // lvalue reference
  X & lr1 = x ;    // 1. OK
  X & lr2 = X() ;   // 2. Error

  // rvalue reference
  X && rr1 = x ;   // 3. Error
  X && rr2 = X() ;  // 4. OK
}

rvalueのオブジェクトには名前がなく、参照されなくなった時点で、自動的に破棄されるものである。勝手に破棄されるなら、書き換えても無駄である。constではなくなったからといって、何がそんなに嬉しいのか。

http://cpplover.blogspot.jp/2009/11/rvalue-reference_23.html

rvalue reference とは、参照の一種で、「今後 使われないオブジェクト1」を参照するものです。

http://d.hatena.ne.jp/gintenlabo/20110116/1295195945

右辺値参照の存在理由

rvalueのオブジェクトには名前がなく、参照されなくなった時点で、自動的に破棄されるものである。勝手に破棄されるなら、書き換えても無駄である。constではなくなったからといって、何がそんなに嬉しいのか。

http://cpplover.blogspot.jp/2009/11/rvalue-reference_23.html

右辺値参照型と左辺値参照型は,

  • 異なった型として扱われるのと,
  • 関数の戻り値型 (とキャストの型) に指定された場合に,その関数の呼び出し結果が右辺値として扱われるか左辺値として扱われるかが異なるのと,
  • 参照への参照をただの参照に変換する規則 (reference collapsing rule) において振る舞いが異なるのと,
  • 初期化の semantics が違うのと,
  • 関数のパラメタ型に指定されている場合のオーバーロード解決の規則が異なるのと,
  • 関数テンプレートのパラメタ型として指定された場合に,型パラメタの推論の仕方が異なる

のを除いて従来からの参照と振る舞いが全く一緒.

http://d.hatena.ne.jp/Cryolite/20080220

一度でも名前がついたオブジェクトは明示的に変換をかけない限り常に左辺値として扱われる.この点に関しては,そのオブジェクトへの参照の型が右辺値参照型か左辺値参照型かはまったく関係ない.

void f(int &i)
{
  i; // 式 i の結果は左辺値として扱われる
}

void f(int &&i)
{
  i; // 式 i の結果は左辺値として扱われる
}

重要なのは,ある参照が右辺値参照型か左辺値参照型かではなくて,ある式が右辺値なのか左辺値なのか.

http://d.hatena.ne.jp/Cryolite/20080220

右辺値参照というのは今保持している値が一時オブジェクトであるというこを明確にするために存在している。右辺値参照というのは「この変数は一時オブジェクトですよ」ということをコンパイラに教え、「コピー」ではなく「ムーブ」に処理をスイッチさせるためにあるのだ。

コピーとムーブの違い

「コピーは複製なので所有権を委譲できず、同一のオブジェクトを作らなければならない」 が、 「ムーブは移動なので所有権を委譲でき、同一のオブジェクトを作るのではなく所有権を移動する(しなければならない)」

http://isoparametric.hatenablog.com/entries/2009/12/16

以下のようなクラスを考える。

class X
{
private :
  char * ptr ;

public :
  X()
  {
    ptr = new char[1000] ;
    // バッファに対して、時間のかかる書き込みを実行
  }

  // コピーコンストラクタ
  X( X const & r )
  {
    ptr = new char[1000] ;
    std::copy( &ptr[0], &ptr[1000], &r.ptr[0] ) ;
  }

  // デストラクタ
  ~X()
  {
    delete[] ptr ;
  }

} ;

このクラスは、明らかにコンストラクタとコピーコンストラクタの実行が遅い。もし、コピーコンストラクタを、ポインタのすげ替えだけにすれば、パフォーマンスが大いに向上するだろう。ところが、そんなことをしてしまっては、コピー元のオブジェクトが使えなくなってしまうので、それは出来ない相談である。

しかし、よく考えると、安全に、コピーをポインタのすげ替えだけで済ませられる場合が存在するのである。

struct X {} ;

X f(){ return X() ; }

int main()
{
    // 関数の戻り値はrvalueである。
    X a( f() ) ; // 1.

    X tmp ; 
    X b( tmp ) ; // 2.

    // これ以降、tmpはもう使わない。
}

ここで、関数の戻り値はrvalueなので、安全にポインタをすげ替えられる。また、tmpは、もうこれ以上使わないので、ポインタをすげ替えても差し支えない。

http://cpplover.blogspot.jp/2009/11/rvalue-reference_23.html

上記の例のようなポインタのすげ替えをムーブと呼ぶ。その後その一時オブジェクト内で使用されないリソースはコピー先に渡してしまってもよいのである。

ムーブ・コンストラクタ

前セクションから、本の虫さんの「rvalue reference 完全解説」の引用を続ける。というか普通のC++使いであればこの記事を読めばrvalueに関してはほぼ完璧に理解できるはずだ。私は残念ながらC++使いではないので、難しい内容だが。

Move コンストラクタ

1. のコピーを、ポインタのすげ替えにするために、クラスXに、rvalue referenceを引数に取るコンストラクタを追加する。

class X
{
public :

// Move コンストラクタ

  X( X && r )   {     ptr = r.ptr ;     r.ptr = nullptr ;   } } ;

これをmoveコンストラクタと呼ぶ。1. は、このmoveコンストラクタが呼ばれ、ポインタのすげ替えになる。コピー元のオブジェクトのポインタを、nullptrにするのを忘れないこと。さもなくば、デストラクタが走る際に、ランタイムエラーになるだろう。

lvalueをmoveせよ

さて、2. はどうしたらいいだろう。moveコンストラクタを実装したものの、コンパイラは2. の場合には、moveコンストラクタを呼び出してくれない。なぜなら、コンパイラは、プログラマの脳内仕様を読んではくれないからだ。tmpが、その後に使われていないかどうかは、コンパイラは静的に決定できないのである。

そこで、プログラマが意図を伝えてやらなければならない。

で、この左辺値(lvalue)であるtmpを一時オブジェクト(rvalue)だということを伝えるために、std::moveがある。

std::move

std::moveは引数として渡されたオブジェクトへの右辺値参照を返すテンプレート関数である。これを使えばコンパイラに対して右辺値であることを明示することができる。先ほどの本の虫さん2.であれば、「X b( std::move(tmp) ) ;」と書けば、ムーブコンストラクタが呼ばれるようになる。

ムーブ・セマンティクス

コピー(一時オブジェクト)を作成せず、ポインタの移動しようというもの

http://d.hatena.ne.jp/faith_and_brave/20071004/1191497817

右辺値参照はいつでも破壊してよいことを利用して、いわゆる「破壊的コピー」を行うのが、ムーブセマンティクスです。 代表的な用途はムーブコンストラクタですが、別にコンストラクタに限る必要はありません。普通の関数の中で、引数を破壊しながら処理したっていいのです。

http://d.hatena.ne.jp/cflat-inc/20130121/1358725785

「ムーブセマンティクス」とは、関数の戻り値のような一時オブジェクトは破棄されることがあらかじめ判っているから、ならば、その一時オブジェクトが握っているリソースを受け側のリソースとして「つなぎ替える」ことでリソースを複写をせずに、受け手へ高速に値を渡す手法。

http://d.hatena.ne.jp/A7M/20090127

ムーブ・セマンティクスはオブジェクトのコピーを伴う操作時に一時オブジェクト内のリソース生成コストをなくすイディオムである。これは右辺値参照がなくても実装可能だが、右辺値参照があればより簡潔・明快にコードが書けるのである。

C++11標準ライブラリとムーブ

C++標準ライブラリは、文字列/標準コンテナ/スマートポインタなどのプログラム基礎部品として使えるクラス群を提供します(正確には“クラステンプレート”で提供されますが、簡単のためクラスで説明を続けます)。C++11標準ライブラリで提供されるクラス群は、基本的に全てムーブセマンティクスに対応しました。これはC++11で新規追加されたクラスstd::unique_ptrだけでなく、C++03時代から存在する馴染みのクラスstd::stringやstd::vectorなども、C++11ではムーブセマンティクスに対応したという意味です。

http://yohhoy.hatenablog.jp/entry/2012/12/15/120839

そういうことで、C++11の標準ライブラリはムーブセマンティクスに対応しているので安心して使用できる。Boostライブラリもそうなんだろうね。