【C++】ムーブセマンティクス(std::move)とは?|無駄なコピーをなくす仕組み

C++

今回は、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 を渡す場面から、自然に使ってみてください。

関連記事