std::moveはデータ移動しない問題 【C++トラップ#1】


はじめに

対象

以下に当てはまる方に向けた記事です。

  • C++の入門は既に終えた
  • std::moveをそこそこ使っていて、無駄なコピーを避けたい
  • Cスタイルの配列を使っている
  • ポインタの使い方が合っているか不安
  • 無駄なコピーがいつ起こるのか分からない

注意点

  • クラスclass/構造体structの意味合いの違いはないです。同じものとしてみてください。
  • 本文には筆者の主観的感想が多少入っています。事実とは限らないこと、確認が不足しているところなどは注釈でお知らせします。
  • 筆者は記事を書くのに慣れていないため見づらいと思います。すいません。

std::moveの意味の勘違い

C++入門ではよく、「std::move自体はデータの移動はしない」という言葉を耳にします。
入門中の時点では「なるほどねー」くらいにしか思わないですが、よくよく考えてみると少し不安な言葉です。

ある程度クラスを使うようになると、std::moveを使用してコピーを避けるといったことをします。
ただ、std::moveの使い所をよく理解していないと、C++では不要なコピーが大量に発生してしまう可能性があります。

moveの単語の意味合い的に、std::moveの仕様を勘違いしやすいため、入門を終え、クラス/構造体をある程度使うようになった、C++初級者~中級者程度向けに改めて解説しようと思います。

間違えたstd::moveの使い方

もしこんなコードを書いているのなら無駄なコピーが発生している可能性が高いです。

例として、スカラー型のメンバを大量に持つ、巨大な構造体Hugeがあるとします。[1][2]

struct Huge {
  char name[256];
  int age;
};

このとき、以下は無駄なコピーが発生してしまうコードです。

ex1.cpp
Huge a;
Huge b = std::move(a);
ex2.cpp
struct Foo {
  Huge mHuge;
  Foo(Huge&& huge)
    : mHuge(std::move(huge))
  {}
};

Huge a;
Foo b { std::move(a) };
ex3.cpp
Huge a;
Huge b;
std::swap(a, b);

ex3.cppstd::swapを使用していますが、std::swapは内部的にstd::moveを使用しています。[3]

ex2.cppは、実際に筆者がコードを書いていてstd::moveの意味を良く調べていなかったことを直感したコードでした。
実際にex2.cppの書き方ではHuge丸々コピーされてしまいます。[1:1]

つまり

  • 正しいクラスの作り方
  • 正しいstd::moveの意味

この両者をしっかり理解していないと、いくらstd::moveで無駄なコピーを避けていても、それが無意味になってしまう事があります

C++は「速い」「効率が良い」と言われますが、その速さ効率を出すためには小さなミスにも気を使わなければなりません。
「C++を中途半端に使う」ということが他の高級言語に比べて危険であることがあります。

std::moveを使用しても、コピーが発生する」ということに関して、次の節で詳しく説明します。

std::moveの使い方

構造体での例

次のような巨大な構造体Hugeとそのインスタンスaがあるとします。[1:2]

struct Huge {
  char name[256];
  int age;
};

あるインスタンスab移動したい場合次のようにします。

Huge a;
Huge b = std::move(a);


これだとabコピーしたのと同じパフォーマンスになってしまいます。[1:3]
これは無駄なコピーであって、望ましくない挙動をする可能性があります。

C言語のころの対策

そこで、C言語のコードではよく、構造体を扱う際はポインタを使用します。

Huge *a = malloc(sizeof(Huge));

free(a);

ポインタを使えば、他の変数に渡す際も本体のコピーは発生せず、ポインタのコピーだけで済むため、データのコピーは最小限に納めることが出来ます

Huge *b = a;

ただ、unique_ptrなどのスマートポインタがないC言語では、生のポインタをそのまま使うしかないので、多重解放メモリリークと常に向き合う必要がありました。

free(b);
free(a); // 多重解放

スマートポインタを使う

さっきのC言語のコードをC++でのスマートポインタに置き換えます。

std::unique_ptr<Huge> a = std::make_unique<Huge>();
std::unique_ptr<Huge> b = std::move(a);

aからbに所有権を移すために、std::moveを使用しています。

ここで大切なのが、この場合std::moveは「所有権の移動」をしているだけであって、「データの移動」をしているわけではないということです。

スマートポインタを使えば、無駄なコピーを避け、効率的なオブジェクト管理が出来ます。


解決法1「ポインタ(スマートポインタ)を使う」

以上のことから、「ポインタ(スマートポインタ)を使い、構造体/クラスの無駄なコピーを避ける」という一つの解決法があるということがわかりました。

ですが、C++を使う上ではクラス使用者側の配慮以外に、クラス提供者側の配慮が大切です。

スマートポインタを使うのもいいですが、もう1ランクレベルアップのために、このクラス提供者側の配慮を意識してみます。(解決法2)

解決法2「クラス側で配慮する」

クラス提供者側の配慮は「正しいクラスの作り方・在り方」で説明する予定でしたが、少し本題から外れるため、他の記事で書くことにします。(完成したらここにリンク貼ります)

まとめ

以上のことから、「std::move = データの移動」というのは実質、間違えた解釈でした。
最初にも説明したが、入門書によくある通り「std::move自体はデータの移動はしない」のです。
これを都合よく「std::move = データの移動」という解釈をし、使っているC++プログラマーは過去の筆者のように多少なり居ると思います…

このstd::moveの意味合いの問題は特に見落としがちなトラップですが、
それ以外にも、C++には数々のトラップが隠されています。
そんな「危険な言語を中途半端に使う」ということはバグエラーを引き起こす原因になります。

どんなプログラマでもミスをし、エラーを生み出します。
ただ、無駄なコピーの問題はエラーどころか警告も説明も出ない、厄介なバグであり、パフォーマンスを著しく低下させる要因の一つです。

筆者は、そんなC++よりも、モダン安全な言語を使用したほうが、結果的に開発効率も実行効率も良くなる可能性が充分あると思います。

今後の開発の参考になれば幸いです。
ありがとうございました。

脚注
  1. メンバが配列だったり、単純な場合、処理系によってはコピーと同等か、そうでないか分かれてしまうので、ただメンバがいっぱいあってそれが全部1個1個コピーされてしまう可能性のあるクラスと思ってください。 ↩︎ ↩︎ ↩︎ ↩︎

  2. 通常はこんな構造体は避けるべきで、char[256]→std::stringの様に、効率の良い書き方をします。そして、std::stringならコピーはされません。ただ、スカラー型のメンバがそこそこある(ゲームの場合は多くなることがある)クラスでは、大量のコピーが起こってしまいます。 ↩︎

  3. C++標準の関数・クラスなどは内容が言語規格で定まっていないため、必ずstd::moveを使用するとは限らない。(コンパイラ独自の関数を使っている可能性がある) ↩︎