ムーブセマンティクス
『C++11』で導入されたムーブセマンティクスは、オブジェクトのリソースを「コピー」ではなく「移動」することで、不要なメモリ確保とデータコピーを回避する仕組みです。std::move を使うと左辺値を右辺値参照にキャストでき、ムーブコンストラクタ・ムーブ代入演算子が呼ばれます。
構文
// ========================================
// ムーブセマンティクスの基本構文
// ========================================
#include <utility> // std::move を使うために必要です
class MyClass {
public:
// ムーブコンストラクタ(右辺値参照 && を受け取ります)
MyClass(MyClass&& other) noexcept {
// other のリソースを奪い取ります(コピーは発生しません)
// other は「空の状態」にしておきます
}
// ムーブ代入演算子
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) {
// 自分のリソースを解放してから other のリソースを移動します
}
return *this;
}
};
MyClass a;
MyClass b = std::move(a); // a のリソースを b に移動します
// 移動後の a は有効だが不定状態になります
MyClass c;
c = std::move(b); // b のリソースを c にムーブ代入します
構文一覧
| 構文・概念 | 概要 |
|---|---|
| 右辺値参照(T&&) | 一時オブジェクト(右辺値)を束縛するための参照型です。ムーブコンストラクタやムーブ代入演算子の引数に使います。 |
| ムーブコンストラクタ | T(T&& other) の形式で定義します。other のリソースを奪い取り、コピーを発生させずにオブジェクトを構築します。 |
| ムーブ代入演算子 | T& operator=(T&& other) の形式で定義します。自己代入チェック後に既存リソースを解放し、other のリソースを移動します。 |
| std::move | 左辺値を右辺値参照にキャストするユーティリティ関数です。移動後の元オブジェクトは「有効だが不定」な状態になります。 |
| noexcept | ムーブ操作は例外を投げないことを宣言します。標準コンテナがムーブを優先的に使うために重要です。 |
| std::vector のムーブ | push_back や emplace_back で一時オブジェクトを渡すと自動的にムーブが適用され、不要なコピーが避けられます。 |
サンプルコード
dragonball_move.cpp
// ========================================
// dragonball_move.cpp
// ドラゴンボールのキャラクターの「戦闘力データ」を
// ムーブセマンティクスで効率的に管理するサンプルです
// ========================================
#include <iostream>
#include <string>
#include <vector>
#include <utility> // std::move
// ========================================
// PowerData クラス
// 動的配列で戦闘力の履歴を保持します
// コピーは高コストなため、ムーブを活用します
// ========================================
class PowerData {
private:
std::string characterName; // キャラクター名です
int* history; // 戦闘力履歴(動的配列)です
int count; // 履歴の件数です
public:
// 通常コンストラクタ
PowerData(const std::string& name, int* powers, int n)
: characterName(name), count(n) {
history = new int[count];
for (int i = 0; i < count; i++) {
history[i] = powers[i];
}
std::cout << "[コンストラクタ] " << characterName
<< " の戦闘力データを確保しました。" << std::endl;
}
// コピーコンストラクタ(比較のために定義します)
PowerData(const PowerData& other)
: characterName(other.characterName), count(other.count) {
history = new int[count];
for (int i = 0; i < count; i++) {
history[i] = other.history[i];
}
std::cout << "[コピー] " << characterName
<< " の戦闘力データをコピーしました。(重い処理です)" << std::endl;
}
// ムーブコンストラクタ
// other のポインタをそのまま奪い取るのでメモリ確保が発生しません
PowerData(PowerData&& other) noexcept
: characterName(std::move(other.characterName)),
history(other.history),
count(other.count) {
// other が delete しないようにポインタを null にします
other.history = nullptr;
other.count = 0;
std::cout << "[ムーブ] " << characterName
<< " の戦闘力データをムーブしました。(軽い処理です)" << std::endl;
}
// ムーブ代入演算子
PowerData& operator=(PowerData&& other) noexcept {
if (this != &other) {
// 自分が持つリソースを解放します
delete[] history;
// other のリソースを移動します
characterName = std::move(other.characterName);
history = other.history;
count = other.count;
// other を安全な空状態にします
other.history = nullptr;
other.count = 0;
std::cout << "[ムーブ代入] 戦闘力データをムーブ代入しました。" << std::endl;
}
return *this;
}
// デストラクタ
~PowerData() {
delete[] history;
// nullptr の delete は安全なので問題ありません
}
// 戦闘力履歴を表示します
void show() const {
if (history == nullptr) {
std::cout << "(ムーブ済みのため空です)" << std::endl;
return;
}
std::cout << characterName << " の戦闘力履歴: ";
for (int i = 0; i < count; i++) {
std::cout << history[i];
if (i < count - 1) {
std::cout << " → ";
}
}
std::cout << std::endl;
}
};
int main() {
std::cout << "=== ドラゴンボール 戦闘力データ管理 ===" << std::endl << std::endl;
// --------------------------------------------------
// 通常のコンストラクタで各キャラのデータを作成します
// --------------------------------------------------
int gokuPowers[] = { 416, 8000, 90000, 3000000 };
int vegetaPowers[] = { 18000, 24000, 250000, 1500000 };
int piccoloPowers[] = { 322, 3500, 40000 };
int gohanPowers[] = { 1307, 10000, 2000000 };
int freezaPowers[] = { 530000, 1000000, 120000000 };
PowerData goku("孫悟空", gokuPowers, 4);
PowerData vegeta("ベジータ", vegetaPowers, 4);
PowerData piccolo("ピッコロ", piccoloPowers, 3);
std::cout << std::endl;
// --------------------------------------------------
// コピーとムーブの比較
// --------------------------------------------------
std::cout << "--- コピー vs ムーブ ---" << std::endl;
// コピーコンストラクタ(新しいメモリを確保します)
PowerData gokuCopy = goku;
// ムーブコンストラクタ(メモリ確保なし・ポインタの付け替えのみ)
PowerData gokuMoved = std::move(goku);
std::cout << std::endl;
// ムーブ後の元オブジェクトは空になります
std::cout << "ムーブ後の goku: ";
goku.show(); // nullptr のため「空です」と表示されます
std::cout << "コピーした gokuCopy: ";
gokuCopy.show();
std::cout << "ムーブした gokuMoved: ";
gokuMoved.show();
std::cout << std::endl;
// --------------------------------------------------
// ムーブ代入演算子
// --------------------------------------------------
std::cout << "--- ムーブ代入 ---" << std::endl;
PowerData gohan("孫悟飯", gohanPowers, 4);
PowerData freeza("フリーザ", freezaPowers, 3);
std::cout << std::endl;
// freeza のデータを gohan にムーブ代入します
gohan = std::move(freeza);
std::cout << std::endl;
std::cout << "ムーブ後の freeza: ";
freeza.show(); // 空になります
std::cout << "ムーブ代入後の gohan: ";
gohan.show(); // フリーザのデータになります
std::cout << std::endl;
// --------------------------------------------------
// std::vector への push_back でムーブを活用します
// --------------------------------------------------
std::cout << "--- vector への push_back ---" << std::endl;
std::vector<PowerData> team;
// std::move を使うとコピーではなくムーブが発生します
team.push_back(std::move(vegeta));
team.push_back(std::move(piccolo));
std::cout << std::endl;
std::cout << "チームのデータ:" << std::endl;
for (int i = 0; i < (int)team.size(); i++) {
std::cout << " ";
team[i].show();
}
return 0;
}
# コンパイルします g++ -std=c++11 dragonball_move.cpp -o dragonball_move # 実行します ./dragonball_move === ドラゴンボール 戦闘力データ管理 === [コンストラクタ] 孫悟空 の戦闘力データを確保しました。 [コンストラクタ] ベジータ の戦闘力データを確保しました。 [コンストラクタ] ピッコロ の戦闘力データを確保しました。 --- コピー vs ムーブ --- [コピー] 孫悟空 の戦闘力データをコピーしました。(重い処理です) [ムーブ] 孫悟空 の戦闘力データをムーブしました。(軽い処理です) ムーブ後の goku: (ムーブ済みのため空です) コピーした gokuCopy: 孫悟空 の戦闘力履歴: 416 → 8000 → 90000 → 3000000 ムーブした gokuMoved: 孫悟空 の戦闘力履歴: 416 → 8000 → 90000 → 3000000 --- ムーブ代入 --- [コンストラクタ] 孫悟飯 の戦闘力データを確保しました。 [コンストラクタ] フリーザ の戦闘力データを確保しました。 [ムーブ代入] 戦闘力データをムーブ代入しました。 ムーブ後の freeza: (ムーブ済みのため空です) ムーブ代入後の gohan: フリーザ の戦闘力履歴: 530000 → 1000000 → 120000000 --- vector への push_back --- [ムーブ] ベジータ の戦闘力データをムーブしました。(軽い処理です) [ムーブ] ピッコロ の戦闘力データをムーブしました。(軽い処理です) チームのデータ: ベジータ の戦闘力履歴: 18000 → 24000 → 250000 → 1500000 ピッコロ の戦闘力履歴: 322 → 3500 → 40000
よくあるミス: ムーブ後のオブジェクトを使い続ける
std::move でリソースを移動した後の元オブジェクトは「有効だが不定な状態」です。ムーブ後のオブジェクトのメンバにアクセスしたり、デストラクタで二重解放が起きないよう、移動先でポインタを nullptr にする処理は必須です。
// NG: ムーブ後の goku を使い続けています
PowerData goku("孫悟空", powers, 4);
PowerData gokuMoved = std::move(goku);
goku.show(); // ムーブ後は不定状態のため動作が未定義です
OK: ムーブ後は元オブジェクトを使わないようにします。ムーブコンストラクタ内でポインタを nullptr にして安全な空状態にします。
// OK: ムーブコンストラクタでポインタを nullptr にします
PowerData(PowerData&& other) noexcept
: history(other.history), count(other.count) {
other.history = nullptr; // 二重解放を防ぎます
other.count = 0;
}
よくあるミス: noexcept を付けないためコンテナがムーブを使わない
std::vector などの標準コンテナは再確保時に、ムーブコンストラクタが noexcept でない場合はコピーコンストラクタを使います。noexcept を付け忘れるとパフォーマンス上の利点が得られません。
// NG: noexcept がないため vector はコピーを使います
PowerData(PowerData&& other) { // noexcept がありません
// ...
}
OK: ムーブコンストラクタとムーブ代入演算子に noexcept を付けます。
// OK:
PowerData(PowerData&& other) noexcept {
// ...
}
PowerData& operator=(PowerData&& other) noexcept {
// ...
return *this;
}
概要
ムーブセマンティクスは『C++11』で導入された機能で、オブジェクトのリソース(ヒープメモリ・ファイルハンドルなど)をコピーせずに「所有権ごと移動」することで、パフォーマンスを大幅に改善できます。ムーブコンストラクタ(T(T&& other))とムーブ代入演算子(T& operator=(T&& other))を定義し、std::move で左辺値を右辺値参照にキャストすることで呼び出せます。ムーブ後の元オブジェクトは「有効だが不定な状態」になるため、移動後にポインタを nullptr にするなど安全な空状態にしておくことが重要です。noexcept を付けると std::vector などの標準コンテナがコピーよりムーブを優先的に選択するため、実際のパフォーマンス向上につながります。コピーコンストラクタが「データの複製」であるのに対し、ムーブコンストラクタは「ポインタの付け替え」だけで済むため、大きなデータを扱うクラスほど効果が大きくなります。
記事の間違いや著作権の侵害等ございましたらお手数ですがこちらまでご連絡頂ければ幸いです。