野良C++erの雑記帳 このページをアンテナに追加 RSSフィード Twitter

2011-01-16 「C++さんのことなんかぜんぜん好きじゃないんだからねっ!!」

C++0x 標準ライブラリ完全解説 〜 No.02 std::move, <utility>

該当規格: 20.3.3 forward/move helpers [forward], N3225

http://sites.google.com/site/cpprefjp/reference/utility/move


少し間隔があいてしまいましたが、 C++0x 標準ライブラリ完全解説の二回目です。

前回の記事では、 C++98/03 の時点から変わらないライブラリを紹介しました。

今回からは、 <utility> のうち、 C++0x で追加された部分を取り扱っていきます。

つまり、ここからが C++0x でパワーアップした <utility> の本領発揮。

その最初の回である今回は、 std::move という簡単な、しかし強力なヘルパ関数を紹介します。


これは、あるオブジェクトに対して、

「もうこのオブジェクトは使わないから、好きに扱っていいよ」

と明示する為の関数です。


定義

namespace std {

  template<class T>
  typename remove_reference<T>::type && move( T && t ) noexcept {
    return static_cast<typename remove_reference<T>::type &&>(t);
  }

}

補足

定義だけを見ると、なんとなく難しそうに見えるかもしれませんが、

この関数は要するに、変数を渡した場合には

template<class T>
T&& move( T& t ) {
  return static_cast<T&&>(t);
}

という処理を、一時オブジェクトを渡した場合には

template<class T>
T&& move( T&& t ) {
  return static_cast<T&&>(t);
}

という処理を、それぞれ行います。


解説

引数として渡されたオブジェクトへの rvalue reference を返す関数です。


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

std::move は、引数として渡されたオブジェクト(正確には、オブジェクトへの参照)を rvalue reference へとキャストして返すことで、

そのオブジェクトが「今後は使用されない」ということを明示します。

その結果、 std::move の戻り値として返されたオブジェクトを使う側で、

そのオブジェクトが「今後は使用されない」という点を活かした処理を行うことが可能になります。


具体例を挙げましょう。


以下の関数は、 C++98/03 の範疇で書かれた、渡された std::vector<T> の各要素を二倍する関数です:

template<class T>
void twice( std::vector<T> &v ) {
  std::size_t const n = v.size();
  for( std::size_t i = 0; i < n; ++i ) {
    v[i] *= 2;
  }
}

この関数は効率的ですが、

  • twice(v) という関数呼び出しから、 v の変更可能性を読み取ることが出来ない
    • かといってポインタを取る関数にして twice(&v) と書くのは馬鹿らしい
  • 一時オブジェクトを渡すことが出来ない

といった弱点があります:

// [ 0, n ) の vector を作る
std::vector<int> make_vector( int n )
{
  std::vector<int> v;
  v.reserve(n);
  
  for( int i = 0; i < n; ++i ) {
    v.push_back(i);
  }
  
  return v;
}

// make_vector(n) で得られる vector の各要素を二倍したものを返す
std::vector<int> f( int n )
{
  std::vector<int> v = make_vector(n);
  twice(v);
  return v;
  
  // return twice( make_vector(n) );
  // と書きたいけど、無理
}

この弱点に対処するため、 twice を以下のように書き換えた場合、

std::vector<int> twice( std::vector<int> v )
{
  std::size_t const n = v.size();
  for( std::size_t i = 0; i < n; ++i ) {
    v[i] *= 2;
  }
  
  return v;
}

// [ 0, n ) の vector を作る
std::vector<int> make_vector( int n )
{
  std::vector<int> v;
  v.reserve(n);
  
  for( int i = 0; i < n; ++i ) {
    v.push_back(i);
  }
  
  return v;
}

// make_vector(n) で得られる vector の各要素を二倍したものを返す
std::vector<int> f( int n ) {
  // 明示的に変数を作らなくても大丈夫になった
  return twice( make_vector(n) );
}

場合によっては、無視できないレベルのコピーコストがかかってしまう場合があります。


例を挙げましょう。

int main()
{
  std::vector<int> v;
  
  // ここで v に大量の要素を格納する
  // ...
  
  v = twice(v); // v の中身を二倍する
  
  // 更新された v を使って処理を続行
  // ...
}

上記のコードの twice(v) は、 v の中身を全てコピーして twice に渡すため、効率が悪いコードです。

何故 v の中身をコピーするかというと、仮に v = twice(v); というコードが、

std::vector<int> t = twice(v);

というコードだった場合、 v は今後も使われるため、その中身が書き換えられるのは問題だからです。


この問題に対処するには、最初に定義したように、参照を取る関数にする、というのが、 C++98/03 での解決策でした。

一方で C++0x では、それに加え、もう一つの解決策が用意されています。

それが、 std::move を使って 「その変数が今後は使われない」ことを明示する、という方法です。

// twice の定義は全く変更していない
std::vector<int> twice( std::vector<int> v )
{
  std::size_t const n = v.size();
  for( std::size_t i = 0; i < n; ++i ) {
    v[i] *= 2;
  }
  
  return v; // ここは大体のコンパイラでは NRVO によって最適化される
            // 仮に NRVO が働かなくても、 std::move(v) と書く必要はない
            // ここの v は、もう使われないことが分かりきっているため
}

int main()
{
  std::vector<int> v;
  
  // ここで v に大量の要素を格納する
  // ...
  
  // v の中身を二倍する
  // v の中身は再代入されるので、どのように変更されても問題ない
  v = twice( std::move(v) ); // そこで、 std::move によって、その旨を通知する
  
  // 更新された v を使って処理を続行
  // ...
  
  // 変更されたくない場合には、 move を使わなければいい
  auto t = twice(v);
  
  // t と v を使って処理を続行
  // ...
}

こう書いた場合、 twice の関数呼び出しにおいて、 v の中身はコピーされません。*3

何故なら、 twice 内で v が初期化される際に呼ばれる std::vector<T> のコンストラクタは、引数として rvalue reference が渡された時、与えられたオブエジェクトの中身をコピーせず、中身を単純に移動させる*4からです。


このように、 std::move を使って「そのオブジェクトが今後は使われない」旨を明示することは、一般に、

プログラムの読みやすさや関数の使いやすさを保ったまま、効率を大幅に改善することを可能にします。


ここで注意して欲しいのは、別に std::move を使えば 即 効率がよくなるとは限らない、ということです。

std::move を始めとする rvalue reference は、あくまで、

「このオブジェクトは今後は使用されないよ(だから好きに書き換えていいよ)」

という旨を明示するためのものであって、実際に行える効率改善は、従来の参照あるいはポインタを使った効率改善と、何ら変わりません。


今回の例で言えば、 std::move を使わなくても、

引数として参照を使えば、効率を改善することは簡単に出来ます。

std::move を使うことで生まれるメリットは、効率を改善すること自体よりも、

プログラムの読みやすさ、あるいは関数の使いやすさを保ったままプログラムを改善できること、

こちらの方が大事である、と考えてください。


実際には、 rvalue reference によって行えるようになることは、効率改善以外にもあるのですが、*5

それをひっくるめて、「読みやすさを保ったまま」出来ることを増やす(例えば効率改善など)ことこそが、

std::move を始めとする rvalue reference の持つ魅力なのです。*6


そして、実のところ rvalue reference は、使うだけならば特に難しくはありません。

というのも、 rvalue reference というのは、大半の部分で、明示的に std::move を使わなくても動作するからです。

std::vector<int> twice( std::vector<int> v )
{
  std::size_t const n = v.size();
  for( std::size_t i = 0; i < n; ++i ) {
    v[i] *= 2;
  }
  
  return v; // ここの v は自動的に move される
}

std::vector<int> g()
{
  std::vector<int> x, y;
  
  // x に適当に格納
  
  y = twice( std::vector<int>{ 1, 3, 5 } ); // twice の引数も、結果も、ともに move される
  
  // x と y を使って色々処理
  
  return twice( std::move(x) ); // x は関数呼び出しを挟んでいるので明示的に move する必要があるが
                                // twice の結果は自動的に move される
}

std::vector<int> x = std::move( g() );  // 明示的に move しなくても move されるが、
                                        // 明示的に move してもエラーにはならない

このとき、明示的に move しなくても良いものに対して std::move を呼び出しても、コンパイルエラーにはなりません。*7

もっとも、コンパイルエラーにならないだけで、 return x; としている部分を return std::move(x); に書き換えた場合などは、若干動作が変わることもあります*8が、

move を使っている以上、大きなロスにはならないので、問題ないでしょう。


と、長々と書きましたが、そろそろ結論に移りましょう。

std::move というのは要するに、「この値は今後は使用しない」という旨を、明示的に通知する関数です。


その際、実際の動作としては rvalue reference にキャストを行うのですが、

単純に std::move を使う限りにおいては、 rvalue reference について理解する必要はありません。

もちろん、 rvalue reference について正しく理解することは 無駄ではありませんし、

実際の所 rvalue reference というのも別に難しい概念というわけではないのですが、

かといって、 std::move を使う全てのプログラマが rvalue reference を理解している必要があるかといえば、そんな事はないのです。

現に、この記事で書いたコードには、 rvalue reference を表す && は、 std::move の定義の部分以外では、一度も出てきていません。


あくまでも、 std::move というのは、「この値は今後は使わないから、好きに扱ってくれ」という意味であり*9

std::move を使うことで、コードの読みやすさを保ったまま、効率改善を始めとした 様々な恩恵を得ることが出来る、

ということさえ理解しているなら、実用上は問題ないと言えます。


// なお、今回は std::move の説明ということで、 move に対応したクラスを使う方について説明しましたが、

// move に対応したクラスを書く場合でも、 rvalue reference を全て理解している必要はありません。

// というのも、ユーザ定義のコピーコンストラクタ/コピー代入演算子やデストラクタを定義しない限り、

// move 周りは、何も書かなくても、正しく自動生成されるからです。

// C++0xコーディングするにあたって、 rvalue reference を正しく理解する必要があるのは、

// 実際の所、本当にごくごく一部の場合だけだと考えてください。


おまけ(上級者向け)

本文が初心者向けなので、上級者の人向けに、 std::move に関する問題を作ってみました。

std::move に対する理解を深めたい人は、挑戦してみるといいかもしれません。


問1

下記の関数 f は、 std::pair で受け取った引数を、別の関数 g に転送するものである。

template<class A, class B>
void f( std::pair<A, B> p )
{
  // p.first および p.second は、もう使わないので move する
  return g( std::move( p.first ), std::move( p.second ) );
}

関数 f の問題点と、その解決策を述べよ。


問2
struct X
{
  X() {}
  X( X const& ){}
  X( X && ) {}
};
 
#include <utility>
int main()
{
  X const x = {};
  X y = std::move(x);
}

上記のコードでは const オブジェクトを move している。これは well-formed か。

well-formed である場合、 y はどのように初期化されるか、

式 std::move(x) の型と value category を示した上で、簡潔に説明せよ。


問3

C++0x における const*10の持つ問題点を挙げ、

その問題点に対する解決策を、

のそれぞれついて述べよ。

*1:ここで言う「オブジェクト」とは、 C++ の規格 1.8 The C++ object model [intro.object], N3225 によって定義されている意味での「オブジェクト」であり、プログラムによって生成される、ある型(クラス等のユーザ定義型に限らず、 int や void* といった組み込み型も含みます)の具体的な実体、規格における言い方では メモリ上の領域 a region of storage のことです。なお、参照はメモリ上に実体を持たなくていい( N3225 8.3.2 References [dcl.ref] の 4 に It is unspecified whether or not a reference requires storage. とある)ので、オブジェクトではありません。

*2:それに対し、今後も使用されるオブジェクトに対する参照を、 lvalue reference と言います。 C++98/03 における参照は、専らこちらです。

*3関数 twice の定義が全く変更されていないことに注目してください。

*4:その結果として元のオブジェクトは空になりますが、 rvalue reference は「これ以降は使われない」オブジェクトへの参照なので、どのように変更しても問題にはなりません。

*5関数引数の Perfect Forwarding や std::unique_ptr による安全な move semanctics 等。これらは後の解説記事で説明します。

*6:大事なことなので何回も書きました。

*7: std::move の定義に std::remove_reference を使っているのは、そういう理由です。

*8:具体的には、関数呼び出しが挟まるので、 NRVO が働かなくなります。

*9:大事なことなので何回も(ry

*10:ローカル変数をはじめとした全ての変数に対し、値を変更しない場合には const を付けるという、コーディング上の習慣

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証