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;
};
このとき、以下は無駄なコピーが発生してしまうコードです。
Huge a;
Huge b = std::move(a);
struct Foo {
Huge mHuge;
Foo(Huge&& huge)
: mHuge(std::move(huge))
{}
};
Huge a;
Foo b { std::move(a) };
Huge a;
Huge b;
std::swap(a, b);
ex3.cpp
はstd::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;
};
あるインスタンスa
をb
に移動したい場合次のようにします。
Huge a;
Huge b = std::move(a);
…
これだとa
をb
にコピーしたのと同じパフォーマンスになってしまいます。[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個コピーされてしまう可能性のあるクラスと思ってください。 ↩︎ ↩︎ ↩︎ ↩︎
-
通常はこんな構造体は避けるべきで、char[256]→std::stringの様に、効率の良い書き方をします。そして、std::stringならコピーはされません。ただ、スカラー型のメンバがそこそこある(ゲームの場合は多くなることがある)クラスでは、大量のコピーが起こってしまいます。 ↩︎
-
C++標準の関数・クラスなどは内容が言語規格で定まっていないため、必ず
std::move
を使用するとは限らない。(コンパイラ独自の関数を使っている可能性がある) ↩︎
Author And Source
この問題について(std::moveはデータ移動しない問題 【C++トラップ#1】), 我々は、より多くの情報をここで見つけました https://zenn.dev/fugi/articles/6a280eb5a29b04著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Collection and Share based on the CC protocol