【初心者向け】C++ いい加減ムーブとコピーを理解しよう

C++ムーブとコピーの違いを考える。

C++ いい加減ムーブとコピーを理解する

C++を学んでいると「コピー」と「ムーブ」という言葉が必ず出てきます。
特にスマートポインタやSTLコンテナを使う場面でよく登場し、初心者が混乱しやすいテーマです。

この記事では、コピーとムーブの基本的な違いから入り、なぜムーブが必要なのかを解説します。
難しい実装の細部には踏み込みませんが、「雰囲気」をしっかりつかめるように説明していきます。


コピーとは何か

コピーは、オブジェクトの中身を複製する操作です。
「もう1つ同じものを作る」というイメージで考えるとわかりやすいと思います。

#include <iostream>
#include <string>

int main() {
    std::string a = "Hello";
    std::string b = a;  // これはコピー
    std::cout << a << ", " << b << std::endl; // 出力: Hello, Hello
}

この場合、abはそれぞれ独立した文字列を持っています。
もちろん、bを変更してもaには影響しません。

コピーは便利ですが、オブジェクトが大きい場合にはコストがかかるという欠点があります。


ムーブとは何か

ムーブは、オブジェクトの中身を「移動」する操作です。
複製はせずに、元のオブジェクトから新しいオブジェクトへリソースを渡します。

#include <iostream>
#include <string>
#include <utility> // std::move

int main() {
    std::string a = "Hello";
    std::string b = std::move(a); // ムーブ
    std::cout << "b: " << b << std::endl; // 出力: Hello
    std::cout << "a: " << a << std::endl; // 出力: (空になる可能性が高い)
}

この場合、aの中身は文字通りbに「移動」してしまったため、aは無効な文字列になる可能性が高いです。
(実際は処理系依存で少しの間残る可能性があります。)
ここで重要なのは、コピーと違って新しいオブジェクトを作るのに余計な複製処理が発生しない点です。
効率を優先する場合にはムーブが大きな意味を持ちます。


なぜムーブが必要か

大きなデータ構造を扱うとき

コピーはオブジェクトを丸ごと複製します。
std::vectorstd::stringのように、内部で大量のデータを保持している場合、そのコピーには大きなコストがかかります。

ムーブであれば、中身のポインタを単純に「渡す」だけで済みます。
これにより、大きなデータでも一瞬で所有権を移せるわけです。


関数の戻り値で効率を上げる

C++11以前では、大きなオブジェクトを関数から返すときにコピーが発生していました。
これは非常に非効率です。

std::vector<int> make_large_vector() {
    std::vector<int> v(1000000, 42);
    return v; // C++11以降はここでムーブが働く
}

C++11以降では、関数の戻り値に自動的にムーブが適用されるため、
大規模なデータ構造を効率的に返せるようになりました。


コピーできないオブジェクトの受け渡し

unique_ptrのように「コピー禁止」の設計になっているオブジェクトもあります。
こうしたオブジェクトは所有権の重複を許さないため、コピーはできません。

しかし、ムーブを使えば安全に所有権を渡せるのです。

#include <memory>
#include <iostream>

int main() {
    std::unique_ptr<int> p = std::make_unique<int>(42);

    // std::unique_ptr<int> q = p;         // コピーはできない
    std::unique_ptr<int> q = std::move(p); // ムーブなら可能
    std::cout << *q << std::endl;
}

スマートポインタがムーブと強く結びついている理由の一つです。


STLコンテナでの最適化

std::vectorstd::stringといったSTLコンテナもムーブを積極的に利用しています。
例えばstd::vector::push_backは、引数が一時オブジェクトならムーブで要素を追加します。

std::vector<std::string> v;
v.push_back("temporary"); // 一時オブジェクトはムーブされる

これにより、文字列のコピーをせずに中身を効率的に移すことができます。


関数の引数におけるムーブ(右辺値参照)

C++11以降では、関数の引数として「右辺値参照」を受け取れるようになりました。
これにより、コピーではなくムーブで処理できるケースが増えています。

#include <iostream>
#include <string>

void print_and_consume(std::string&& s) {
    std::cout << s << std::endl;
    // sは右辺値参照なので、ここでリソースをムーブして別に渡せる
}

int main() {
    print_and_consume("Hello"); // 一時オブジェクトは右辺値参照に渡される
}

このように「右辺値参照 (&&)」を使うと、一時オブジェクトやムーブ可能なリソースを効率的に関数に渡せるようになります。
ただし詳細な使い分けや仕組みは少し複雑なので、別記事でまとめる予定です。


例外安全性の向上

コピーよりムーブの方が「例外が起きにくい」という場合があります。
例えばリソースの複製時に失敗する心配があるクラスでも、ムーブであれば単に所有権を移すだけなので例外を投げる可能性が小さくなります。

これもムーブが推奨される理由の一つです。


まとめ

  • コピー:
    オブジェクトを複製する。便利だがコストが高いことがある。
  • ムーブ:
    中身を移動する。効率的で、コピー禁止のオブジェクトでも使える。

ムーブは、

  • 大きなデータ構造を効率的に扱う
  • 関数の戻り値を高速化する
  • コピー禁止のオブジェクトを受け渡す
  • 関数の引数で効率的にリソースを扱う(右辺値参照)
  • STLコンテナの操作を最適化する
  • 例外安全性を高める

といった理由から必要とされています。

スマートポインタの解説は【初心者向け】C++スマートポインタを使ってみよう をぜひ参考にしてください。


こうして見ると、「ムーブ」は単なる便利機能ではなく、現代C++の効率化の根幹を支える仕組みだと分かりますね。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です