What is it, naokirin?

適当にC++0xで使えるようになるMoveコンストラクタを試してみる

#include <iostream>
#include <vector>
#include <memory>

using namespace std;

class Sample
{
public:
  // 普通のctor
  Sample( int x )
  { 
    vec = unique_ptr<vector<int> >( new vector<int>() );
    vec->push_back( x );
  }

  // Move ctor
  Sample( Sample && r )
  {
    // ちなみに、ただのcopy ctorでmoveなんてしたら大変なことになるので注意
    vec = move( r.vec );
  }

  // dtorは空。unique_ptr便利!!
  ~Sample(){}

  // Movable substitution operator
  Sample& operator =( Sample && r )
  {
    // ちなみに、ただのsubstitution operatorでmoveなんてしたら大変なことになるので注意
    vec = move( r.vec );
    return *this;
  }

  // 定義は省略
  Sample( const Sample& );
  Sample& operator =( const Sample& );

private:
  unique_ptr<vector<int> > vec;
};

上のコードのうち

Sample( Sample && r );

が、Moveコンストラクタ

Sample& operator =( Sample && r );

がMovableな代入演算子です。

Move Semantics と std::move()

実際にMoveコンストラクタがなぜ必要なのかというと、コピーに関して非常に多くのデータをもつデータメンバが存在した場合に、そのデータのコピーがボトルネックになる可能性があるということです。

今回の場合であれば、std::copy()関数でコピー元のオブジェクトの vec から、コピー先のオブジェクトの vec にデータをコピーすることになります。ここでコピー元のオブジェクトの vec に大量のデータが存在した場合、そのコピーがボトルネックとなります。

しかし、たとえば今回のようにデータメンバがポインタ、かつコピー元が一時オブジェクトだった場合にはポインタを置き変える(所有権を移動する、Moveする)だけでよいはずです。( コピー元が一時オブジェクトならこれ以降使われることがないはずです。 )

そこでC++0xで導入される、rvalue reference ( 『 T && 』であらわされる参照 ) を用いて、一時オブジェクトの参照が渡された場合にはポインタを置き変えるだけにしたいと思います。( ちなみにrvalue referenceとは、一時的に生成されるオブジェクトへの参照のことです。詳しいことはその他のWebページ、ブログを参考にしてください。 )

これをMove Semanticsと言います。つまり効率的にするために、コピー ( 一時オブジェクト生成 ) ではなく、所有権移動 ( ポインタの移動 ) をしようというわけですね。

あとはその考えに沿ってコードを書いていくと、上に書いてあるコードになります。
ちゃんと引数が『 T & 』と『 T && 』の二つの関数はオーバーロードされます。

ちなみにコメントで「大変なことになる」と書いているのは、「moveされた参照型はその後、使用できなくなる」からです。 ( std::move() が行うことは lvalue を rvalue にキャストしているから当たり前ですね )。rvalue reference ( つまり一時オブジェクトの参照 ) ならそれでも問題ないですが、lvalue reference ( 通常のオブジェクトの参照 ) でこんなことをされたらたまったものではないです。関数を呼び出したら、呼び出し側のオブジェクトが使えなくなるのですから…。

ちなみに Perfect Forwarding と std::forward()

テンプレート関数ではrvalue referenceでlvalue referenceも参照できるようになっていて

template <class T>
Sample f( T && t )
{
  return Sample( t );
} 

のような関数が一つあれば、rvalue referenceもlvalue referenceも引数に取れます。

しかし、このままでは Sample() コンストラクタがたとえ rvalue reference の引数を受け取って効率の良い作業をしてくれようとしても、このままではそのようにできないことになります。
また std::move() を使ってしまうと、lvalue reference を受け取った場合に先ほどと同じ「大変なこと」になってしまいます。

そこで、Perfect Forwardingと呼ばれる手法を用います。
何かとんでもないことをするような言葉の雰囲気がありますが、何のことはない、std::forward() というC++0xで追加されるテンプレート関数を使うだけです。

template <class T>
Sample f( T && t )
{
  return Sample( forward<T>(t) );
} 

これだけです。

これで引数をそのままで Sample() に渡してくれます。( しかもstatic_castを使うより安全らしいです )。

まとめると

Move Semantics と Perfect Forwarding を使うと効率的なコードが書けるようになる。

そして、そのためには std::move() と std::forward() を使いこなす必要がある。

おしまい。