LoginSignup
111
92

More than 3 years have passed since last update.

C++ ムーブセマンティクスと右辺値の概念を初心者向けに

Last updated at Posted at 2019-05-01

備忘録です。

ムーブ、ムーブセマンティクスの概念、ムーブコンストラクタの話を図を交えて解説します。

本記事は、「ムーブという概念は聞いたことがあるんだけど実際どういうものかわからない」という人が対象(のつもり)です。

左辺値と右辺値

ムーブ、ムーブセマンティクスの話をする前に非常に重要な左辺値と右辺値の話をします。

C++では左辺値と右辺値が明確に区別されます。

左辺値と右辺値
int i = 1;

例えばこのコードでは、int型で宣言されているiが左辺値で、1が右辺値になります。

もう少し詳しく説明すると、左辺値int iのような名前付きのオブジェクトのことで、右辺値はすぐに破棄されてしまうような一時的なオブジェクトのことです。
下に右辺値の例を挙げます。

右辺値の例

1; // リテラル
f(); // 参照がない返り値

Object o;
std::move(o) // std::moveを適応させたlvalue

std::moveという見慣れない命令が出てきましたがとりあえず置いておいて...

右辺値は基本的に評価された時点で破棄されてしまいます。
そのため右辺値を生かしておくには右辺値への参照を持つ右辺値参照型という型で束縛する必要があります。

右辺値参照型
int&& r = 1; // 右辺値1を束縛できた!

右辺値参照型はT&&で宣言できます。

一方でObject oのような名前付きのオブジェクトのことを左辺値(lvalue)と呼びます。
左辺値は関数のスコープを外れるまで破棄されません。

ちなみに右辺値参照型は右辺値への参照を持つための型のため、左辺値です。
このため、すぐ破棄されてしまう右辺値を生かすことが可能になります。

スクリーンショット 2019-05-02 0.14.34.png

左辺値を右辺値にキャストする std::move

では、さきほど登場したstd::moveについてです。

std::moveは左辺値を右辺値にキャストします。

#include <utility>

Object o;
std::move(o) // Object o を Object&& oにキャスト!

これでObject oは右辺値へキャストされました。

左辺値を右辺値にして何が嬉しいの?

std::moveは左辺値を右辺値にキャストしますがこれは何が嬉しいのでしょうか?
関数のスコープから外れない限り内容が破棄されないことが保証されている左辺値を、一時オブジェクトと同様の右辺値参照に変更する意味はあるのでしょうか?

では、次のような例を考えてみましょう。

巨大なデータを持つstringを別のstringにコピーする
#include <string>

std::string str("...") // 大量の文字列データを持っているstring型変数

/* strに対して色々な処理を行う */

std::string str_after = str; // 色々な処理を施したstrをstr_afterにコピー!

/* これ以降strに対して処理は行わない */

このプログラムでは大量の文字列データを持ったstrに色々な処理を施した後に str_after という変数にその内容をコピーしています。
つまり str_afterstrのデータをコピーするためにstrのデータ分のメモリを確保する必要があります。

スクリーンショット 2019-05-01 20.59.52.png

動的にメモリを確保するにはOSに必要な分だけのメモリ領域をリクエストする必要があり、C++の処理の中でも時間がかかってしまう部分です。
もしかしたら巨大なデータ領域を新たにリクエストすると、メモリの容量が足りずnullptrを返されるかもしれません。(自分は遭遇したことはありませんが)

プログラム中のstrはコピー後使われないようです。
もう使わないデータならば、下図のようにデータを移動できればいいのに...

スクリーンショット 2019-05-01 22.14.15.png

このとき活躍するのがC++11から新たにクラスに定義できるようになったムーブコンストラクタムーブ代入演算子です。

ムーブコンストラクタとムーブ代入演算子は一般的に以下のように宣言されます。

ムーブコンストラクタ、ムーブ代入演算子
// ムーブコンストラクタ
Test (Test &&right) {}
// ムーブ代入演算子
Test &operator=(Test &&right) {}

ムーブコンストラクタとムーブ代入演算子は引数に右辺値への参照を取ります。
右辺値ということはこの処理が終わり次第、破棄されてしまう可能性が高いオブジェクトです。

引数にそのようなオブジェクトがあるのですからそのままコピーするのはもったいないです。
どうせ捨てられるならその値の所有権を奪ってもいいはずです。

char[1000]を持つクラス
#include <iostream>
#include <cstdlib>
#include <cstring>
using namespace std;

class Test {
private:
  char *p;
public:
  Test(): p(new char[1000]) {};
  ~Test() {
    delete [] p;
  }

  Test(Test const &right) {
    *this = right;
  }

  Test& operator=(Test const &right) {
    if (this != &right) {
      p = new char[1000]; // 新しいメモリ領域を用意
      memcpy(p, right.p, 1000); // コピー
    }

    return *this;
  }

  Test (Test &&right) {
    *this = std::move(right);
  }

  Test& operator=(Test &&right) {
    printf("move assigned!\n");
    if (this != &right) {
      delete [] p; // 自分のメモリ領域を消して...
      p = right.p; // 相手のメモリ領域を譲り受ける
      right.p = nullptr; // 相手のポインタはnullptrに!
    }

    return *this;
  }
};

int main() {
  Test lval1;
  Test lval2;
  Test rval(lval1); // copy constructor
  rval = std::move(lval2); // move代入
}

上に示したのが、ムーブコンストラクタとムーブ演算代入を実装したものです。
ムーブでは相手のポインタを自分のポインタに代入し、さらに相手のポインタをnullptrで初期化しています。

これにより新たにメモリ領域を作らず所有権を奪うことができました。

スクリーンショット 2019-05-02 2.12.04.png

つまり、左辺値にstd::moveを適応させることは適応元の左辺値をムーブ元にしても良いと宣言することと同意です。

この処理以降つかわない!みたいなオブジェクトに使用しましょう。

ただし、ムーブコンストラクタは常に生成されるわけではないことに注意です。
クラスがムーブに対応しているかをチェックすることは非常に大事です。

また、現在のSTLのコンテナは基本的にムーブに対応していますが、ムーブコンストラクタにnoexcept指定がないとコピーコンストラクタが呼ばれてしまうものもある(vectorとか)などの罠もあるため注意が必要です。

ムーブセマンティクス

ムーブセマンティクスとは、左辺値と右辺値が明確に区別されたことによって、ムーブコンストラクタとムーブ代入など値の受け渡し方法に幅ができたこと(と僕は解釈しています)。

まとめ

  • C++において左辺値と右辺値は明確に区別される。
  • 右辺値参照は左辺値
  • std::moveは左辺値を右辺値にキャストする。
  • ムーブコンストラクタとムーブ代入演算子は右辺値参照を引数にとる。

参考文献

Effective Modern C++
C++のムーブと完全転送を知る
cpprefjp

std::forwardと完全転送の仕様をまとめようと思っていたらmoveについて書いていた...

111
92
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
111
92