今回は、C++でよく出てくる「ムーブセマンティクス(std::move)」について解説していきます。
「std::moveって何をしてるの?」
「vectorにpush_backしたら遅い気がする…」
「unique_ptrを渡そうとしたらコンパイルエラーになる…」
こんな疑問はありませんか?
C++は、うっかりすると裏で大量のコピーが走ります。ムーブセマンティクスは、その無駄なコピーをやめて「中身を引っ越す」ための仕組みです。名前は難しそうですが、考え方はとても素直です。
この記事を読み終えると、あなたはムーブの意味・std::moveの正体・vectorやunique_ptrでの使いどころ・初心者がハマりやすいミスをしっかり理解できると思いますので、ぜひ最後まで読んでいただけると嬉しいです。
ムーブセマンティクスとは?
ムーブセマンティクスとは、オブジェクトの中身を「コピーする」のではなく「引っ越す(奪う)」仕組みです。
引っ越しに例えると分かりやすいです。
- コピー … 新居に同じ家具を全部買い直す。元の家もそのまま残る(重い)
- ムーブ … 表札を付け替えるだけ。家具はそのまま、元の家は空になる(軽い)
ポイントは、元のオブジェクトはもう使わないと分かっているときにだけ、この「引っ越し」ができるということです。
なぜムーブが必要?
理由は、コピーが高くつくからです。例えば要素が100万個入った vector をコピーすると、100万個ぶんのメモリを確保して全部詰め直します。
#include <vector>
#include <string>
std::vector<std::string> MakeNames() {
std::vector<std::string> names(1000000, "enemy");
return names;
}
int main() {
std::vector<std::string> a = MakeNames();
std::vector<std::string> b = a; // コピー:100万個ぶん詰め直す(重い)
std::vector<std::string> c = std::move(a); // ムーブ:ポインタを付け替えるだけ(軽い)
return 0;
}b = a は全要素を作り直しますが、c = std::move(a) は内部のポインタを付け替えるだけ。要素数がいくつでも一瞬で終わります。
std::moveの正体
ここが一番の勘違いポイントです。std::move は、実は何も動かしていません。
std::move がやっているのは、「こいつはもう使わないから、中身を奪っていいですよ」という目印を付けるだけです。実際に引っ越しを行うのは、それを受け取った側のムーブコンストラクタやムーブ代入です。
つまり std::move は「移動しろ」ではなく「移動してもいいよ」という許可証だと思ってください。名前が誤解を招くので有名なところです。
実践例:vectorへの追加
ゲームでよくあるのが、作ったオブジェクトを vector に入れる場面です。
#include <iostream>
#include <vector>
#include <string>
class Enemy {
public:
Enemy(std::string name) : name_(std::move(name)) {} // 引数も引っ越す
// コピーコンストラクタ(重い想定)
Enemy(const Enemy& o) : name_(o.name_) {
std::cout << "コピーされたn";
}
// ムーブコンストラクタ(軽い)
Enemy(Enemy&& o) noexcept : name_(std::move(o.name_)) {
std::cout << "ムーブされたn";
}
private:
std::string name_;
};
int main() {
std::vector<Enemy> enemies;
enemies.reserve(4);
Enemy e("slime");
enemies.push_back(e); // コピーされる
enemies.push_back(std::move(e)); // ムーブされる
return 0;
}コピーされた
ムーブされた
同じ push_back でも、std::move を付けるかどうかでコピーかムーブかが変わります。ムーブコンストラクタに noexcept を付けているのも大事で、これがないと vector が再確保のときにムーブを使ってくれません(安全のためコピーを選びます)。
コンテナごとの性質はSTLコンテナ徹底比較の記事もあわせてどうぞ。
実践例:unique_ptrを渡す
unique_ptr はコピーできません(唯一の所有者だから当然です)。なので渡すときは std::move が必須になります。
#include <memory>
#include <vector>
std::vector<std::unique_ptr<Enemy>> enemies;
auto e = std::make_unique<Enemy>("slime");
// enemies.push_back(e); // エラー:コピーできない
enemies.push_back(std::move(e)); // OK:所有権を引き渡す
// この時点で e は空(nullptr)になっている「unique_ptr を渡そうとしたらエラーが出る」の答えがこれです。所有権を手放す意思表示として std::move を書きます。詳しくはスマートポインタの記事をどうぞ。
【重要】私が実際にムーブで困った体験談
個人開発で「std::move を付ければ速くなるらしい」と聞きかじって、あちこちに付けて回ったことがあります。そこでムーブした変数をその後も使ってしまい、中身が空になっていて落ちるというバグを出しました。
std::string name = "slime";
enemies.push_back(std::move(name));
std::cout << name << "n"; // 危険:中身は保証されない(空かもしれない)ムーブした後の変数は「有効だが中身は未規定」という状態です。落ちるとは限らないのが逆に厄介で、手元では動くのに別の環境で壊れました。ムーブしたら、その変数はもう触らないと決めたら事故が止まりました。
もう一つ。return std::move(x); と書いたら逆に遅くなったこともあります。実は普通に return x; と書けばコンパイラが最適化してくれるのに、std::move を付けたせいでその最適化を邪魔していました。よかれと思って書いたのが裏目に出た形です。
ムーブ使用時のよくある失敗例と対処法
①ムーブした後に使ってしまう
体験談のとおりです。ムーブしたら、その変数は捨てたものとして扱う。再利用したいなら、明示的に代入し直しましょう。
②returnにstd::moveを付ける
ローカル変数を返すときは return x; で十分です。std::move を付けると、かえって最適化を妨げます。
③constな変数にstd::moveを付ける
const な変数は中身を奪えないので、std::move を付けても黙ってコピーになります。エラーも警告も出ないので気づきにくい罠です(constの記事もどうぞ)。
注意点
std::moveは移動しない。「奪っていい」という目印だけ- ムーブ後の変数は触らない
- ムーブコンストラクタには
noexceptを付ける(vectorが使ってくれる)
まとめ
- ムーブはコピーせず中身を引っ越す仕組み
std::moveの正体は「奪っていい」という許可証unique_ptrを渡すときはstd::moveが必須- ムーブした変数をその後に使うのが最大の事故
ムーブは、意味が分かれば「無駄なコピーを避ける当たり前の作法」になります。まずは unique_ptr を渡す場面から、自然に使ってみてください。

