Help us understand the problem. What is going on with this article?

一時オブジェクトの寿命と右辺値参照、ムーブセマンティクスのお話

More than 3 years have passed since last update.

仰々しいタイトルですみません。
それぞれが絡み合ってるのであんまり綺麗にまとまっていませんが、一時オブジェクト、右辺値、左辺値、ムーブセマンティクスのお話をします。

4/11 ideoneへのURLをコード例の下に追記しました。
飛ぶと実際に実行した様子を見ることが出来ます。

一時オブジェクト ( = 右辺値 )

一時オブジェクトは、右辺値と同等の意味合いで使われます。
すなわち、関数の戻り値、コンストラクタによって生成された直後のインスタンス、リテラルなどもここに入れることができるでしょう。
基本的に一時オブジェクトはその式( expression )が評価された時点で破棄されます。

struct something { ~something() { std::cout << "destructor" << std::endl; } };

something func() { return {}; }
int main()
{
  something(); // the instance of something is a temporary object
  func(); // the returned value of func() is a rvalue 
  1; // literal
  std::cout << "=== main ===" << std::endl;
}
output:
destructor
destructor
=== main ===

ideoneでの実行例です; http://ideone.com/DIZTpL

somethingがmain関数が終わる前に破棄されていることがわかります。
出力はありませんが、intのデストラクタも=== main === の前に呼ばれています。

これをあたまの隅に置いて、つぎを読んでいただければと思います。

一時オブジェクトの延命と右辺値参照

ここからが本題でしょうか、一時オブジェクトの延命と右辺値参照です。
右辺値参照とは&&で表される型で、一時オブジェクトへの参照を保持します。
注意して欲しいのは、一時オブジェクトへの参照はC++11以前からできていたということです。

struct something { ~something() { std::cout << "destructor" << std::endl; } };

int main()
{
  const something& s1 = something();
  something&& s2 = something();
  std::cout << "=== main ===" << std::endl;
}
output:
=== main ===
destructor
destructor

ideone; http://ideone.com/8AyfwP

main関数が終わってから破棄されているのがわかります。
これが、一時オブジェクトの延命です。
すなわち、一時オブジェクトが普通はその式( expression )が終わった時点で破棄されるところを、参照で受け取ることでその参照型の寿命と同じだけ延命されるというものです。

これは、const参照で昔からできていたことで、みなさんよく使うであろう
void func( const something& s ) { /* dosomething */ }
というような関数があったときに、func( something() )としても正常に動く理由です。

注意するべきは、延命の条件は、延命されるべきオブジェクトがしっかり参照で受け取られていること、ということです。
当然といえば当然ですが、これはときに見つけにくいバグの原因となります。

struct something
{
  int a = 0;
  int& get() { return ( a ); }
  ~something() { std::cout << "destructor" << std::endl; }
};

int main()
{
  int& n = something().get();
  // n has already been destructed.
  std::cout << n << std::endl; // oops!
}

ideone; http://ideone.com/kZl61O

わかるでしょうか。main関数の一行目は、somethingの一時オブジェクトのメンバ変数への参照を得ています。
この時、somethingの一時オブジェクトは延命されていないのです。
なぜなら、somethingの一時オブジェクトそのものは参照によって保持されていないからです。
これはint型なのでそんなバカみたいなクラスを書くか、と思われそうですが、コピーコストのかかるオブジェクトを外部へ公開したい意図があるときにしばしばこんなようなクラスを書く人がいるように思われます。

以下はもう少し分かりづらい例です;

// 危険なコード
for( auto e : something().get_vector() ) // get_vectorは内部に持つvectorへの参照を返す
{
  // e is destroyed
}

// 安全なコード
for( auto e : std::vector < int > { 1,2,3,4,5,6,7,8,9,0 } )
{
  // e is 1,2,3,...,0
}

ideone; http://ideone.com/VsvrZ1

range-based forでは、コンテナへの参照をキャプチャします。上の危険な例ではキャプチャしたものがすぐに破棄されてしまいますが、下の安全な例では、一時オブジェクトをキャプチャして延命しているので安全に利用することができるのです。

また、this参照を返す関数でもこれはやってはいけません。それをキャプチャしても不正な参照になってしまいます。

struct something
{
  something& func() { return *this; }
};

int main()
{
  auto& r = something().func(); // oops!
}

ideone; http://ideone.com/UDsvCd

右辺値参照

const参照で一時オブジェクトの参照ができていたのに、どうして右辺値参照なんて大仰な名前のついたものが新しく導入されたのか。それは、参照しているものが一時オブジェクトかどうかを見分けるためです。
右辺値参照は、一時オブジェクトしか受け取ることができません。

void func( int&& ) {}
int main()
{
  func( 123 ); // ok
  func( int() ); // ok

  int abc = 456;
  int&& def = 789;
  func( abc ); // of course error
  func( def ); // error, def is no longer a rvalue

  func( static_cast < int&& > ( abc ) ); // ok
  func( static_cast < int&& > ( def ) ); // ok
}

int && defが多少わかりにくいかもしれません。defは一時オブジェクトへの参照という変数ですから、一時オブジェクトではありません。ですから、func( def )ももちろんillegalです。
さあ、そうするとキャストして突っ込んでいるのはなんなのか。
右辺値参照へキャストすると、一時オブジェクトという扱いを明示することができるのです。funcに対し、これもう使わないし一時オブジェクトとおんなじように扱ってもいいよ、と教えてあげるのです。
これがstd::moveやstd::forwardの正体です。

ここで初めて、ムーブセマンティクスのお話が出てきます。

ムーブセマンティクス

上を理解していなければここからは読めないかもしれないが、上を理解したならばここを読む必要は最早ない、という微妙な立ち位置にいる項ですが、ムーブセマンティクスのお話をします。

ムーブセマンティクスとは、つまるところただの代入演算子とコンストラクタのオーバーロードです。
引数に受け取った参照が、これからも使うかもしれないので触らないようにするか、もう使わないことが明示されているので破壊するようなことをしてもいいか、という条件で、オーバーロードするのです。

これを手動でオーバーロードするときは、デストラクタでリソースの解放するようなクラスのみであるはずです。メンバ変数のムーブなどは、デフォルトのものがやってくれますからね。
以下は実際の実装ですが、読み飛ばしても問題ありません。

class memory_pool
{
  void* pool = nullptr;
  std::size_t size_pool = 0;

  bool initializer( std::size_t size )
  {
    finalizer();
    size_pool = size;
    return pool = malloc( size );
  }
  void finalizer()
  {
    free( pool );
    pool = nullptr;
    size_pool = 0;
  }
public:
  memory_pool( std::size_t size ) { initializer( size ); }

  // copy
  memory_pool( const memory_pool& right ) { *this = right; }
  memory_pool& operator = ( const memory_pool& right )
  {
    initializer( right.size_pool );
    memcpy( this->pool, right.pool, size_pool );
    return *this;
  }

  // move
  memory_pool( memory_pool&& right ) { *this = std::move( right ); }
  memory_pool& operator = ( memory_pool&& right )
  {
    finalizer();
    this->pool = right.pool;
    this->size_pool = right.size_pool;
    right.pool = nullptr;
    right.size_pool = 0;
    return *this;
  }

  ~memory_pool() { finalizer(); }

  unsigned char& operator [] ( const std::size_t N ) { return ( reinterpret_cast < unsigned char * >( pool )[N] ); }

};

moveした後のオブジェクトの状態は未定義ということなので、どんなゴミが残っていても構いません。ゆえに、上のコードのように全てのメンバ変数を0で掃除しておくようなことをする必要はありませんが、多重解放だけは気をつけてください。moveしてもデストラクタ自体はしっかり変数の寿命に伴って呼ばれます。
つまり、moveされてゴミになったオブジェクトが、move先の新居を解放してしまう可能性があるのです。そして、move先の新居の寿命が尽きた時に晴れて多重解放…なんてことにならないようにしましょう。

実際にmoveを利用するとなると、以下のような動作をすることになります。

int main( int arg, char** argv )
{
  // ただのコンストラクタが呼ばれる。
  // 新しいメモリを確保する。
  memory_pool mp1( 10 );
  for( int i = 0; i < 10; ++i )
  {
    mp1[i] = i; // 確保したメモリへ値を入れていく。
  }

  // コピーコンストラクタが呼ばれる。
  // 新しいメモリを確保し、バイト列を全てコピーする。
  memory_pool mp2 = mp1;

  // ムーブコンストラクタが呼ばれる。
  // 新しいメモリが確保されることはないが、mp1はこれ以降使えない。
  memory_pool mp3 = std::move( mp1 );

  // mp1に新しいオブジェクトを割り当てる
  // 新しいメモリが確保され、再びmp1は使えるようになる
  mp1 = memory_pool( 20 );
  // 一時オブジェクトのデストラクタが呼ばれる

  // main関数終了
// mp3のデストラクタが呼ばれる
// mp2のデストラクタが呼ばれる
// mp1のデストラクタが呼ばれる
}

本当に別段難しいことはないのです。
もっとも、普段はデフォルトのムーブコンストラクタとムーブ代入演算子が、全てのメンバ変数をムーブしてくれるので自分であれこれ考える必要はないのですが……

もしここまで読んでくださった方がいらっしゃいましたら、長々とありがとうございました。

rinse_
C++初心者
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした