C++: ムーブセマンティクス
最終更新:2026-08-31
レッスン44では、マルチスレッド同期について学びました。
ここでは、C++11の最重要機能——ムーブセマンティクスについて学びます。
ムーブセマンティクスを理解することは、C++のパフォーマンス最適化を理解することにつながります。
1. ムーブセマンティクスの基本
(1) 1.1 なぜムーブセマンティクスが必要?
問題: C++98/03では、オブジェクト(右辺値)を返す際に不要なコピーが発生。
▶ サンプル 2:STLコンテナの使用(難易度 ⭐)
std::vector<int> createVector() {
std::vector<int> v = {1, 2, 3, 4, 5};
return v; // ムーブセマンティクスにより最適化
}
std::vector<int> dest = createVector(); // コピーではなくムーブ!
ムーブセマンティクスの解決策: 「一時オブジェクト」から所有権を移転し、コピーを回避します。
(2) 1.2 左辺値と右辺値
| 分類 | 説明 | 例 |
|---|---|---|
| 左辺値(Lvalue) | 名前があり、アドレスを持つ | int x = 10; の x |
| 右辺値(Rvalue) | 一時オブジェクト、名前なし | 10、x + 1、関数の戻り値 |
たとえ話:
- 左辺値 = 名前のある変数(「永続的な存在」)
- 右辺値 = 一時的なオブジェクト(「一瞬の存在」、すぐに消える)
2. 右辺値参照
(1) 2.1 基本的な概念
右辺値参照は && で表し、右辺値にバインドします。
▶ サンプル:右辺値参照(難易度 ⭐)
#include <iostream>
void process(int& x) {
std::cout << "左辺値参照:" << x << std::endl;
}
void process(int&& x) {
std::cout << "右辺値参照:" << x << std::endl;
}
int main() {
int a = 10;
process(a); // process(int&) を呼び出し
process(20); // process(int&&) を呼び出し
return 0;
}
(2) 2.2 std::move
std::move は左辺値を右辺値参照にキャストし、「このオブジェクトを移動してもいい」という意図を表します。
▶ サンプル:std::moveの使用(難易度 ⭐⭐)
#include <iostream>
#include <vector>
#include <utility>
int main() {
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1); // ムーブ、v1の「中身」がv2へ移動
std::cout << "v2のサイズ:" << v2.size() << std::endl; // 3
std::cout << "v1のサイズ:" << v1.size() << std::endl; // 0(v1は空になる)
return 0;
}
std::move自体は何も移動しません、型キャストのみ- 実際の移動はムーブコンストラクタやムーブ代入演算子が行います
3. ムーブコンストラクタとムーブ代入
(1) 3.1 なぜ定義が必要?
問題: コンパイラ生成のデフォルトはディープコピー(クラスがリソースを持つ場合)。
解決策: ムーブコンストラクタとムーブ代入演算子を定義。
(2) 3.2 例:Stringクラスの実装(難易度 ⭐⭐⭐)
#include <iostream>
#include <cstring>
class String {
private:
char* data;
size_t length;
public:
// 通常のコンストラクタ
String(const char* str) {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
std::cout << "構築:" << data << std::endl;
}
// コピーコンストラクタ(ディープコピー)
String(const String& other) {
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
std::cout << "コピー構築:" << data << std::endl;
}
// ムーブコンストラクタ(ムーブ)
String(String&& other) noexcept {
data = other.data; // ポインタを横取り
length = other.length;
other.data = nullptr; // 元のオブジェクトを空にする(二重解放防止)
other.length = 0;
std::cout << "ムーブ構築..." << std::endl;
}
// デストラクタ
~String() {
delete data;
}
};
int main() {
String s1("Hello");
String s2 = std::move(s1); // ムーブコンストラクタを呼び出し
return 0;
}
出力:
TEXT 📖 参照専用構築:Hello ムーブ構築...
4. ムーブセマンティクスのルール
(1) 4.1 五大特殊関数
クラスがデストラクタ、コピーコンストラクタ、コピー代入を定義した場合、ムーブコンストラクタとムーブ代入を定義すべきです。
| 関数 | 説明 |
|---|---|
| デストラクタ | リソース解放 |
| コピーコンストラクタ | ディープコピー |
| コピー代入演算子 | コピー代入 |
| ムーブコンストラクタ | ムーブ |
| ムーブ代入演算子 | ムーブ代入 |
(2) 4.2 ゼアルール
推奨: スマートポインタでリソース管理すれば、五大関数を定義不要(コンパイラ生成のデフォルトで十分)。
class Person {
std::string name; // stringはムーブ対応済み
std::vector<int> scores; // vectorもムーブ対応済み
// 五大関数の定義不要!
};
5. 完全転送
(1) 5.1 完全転送とは?
完全転送(Perfect Forwarding)は、関数の引数を元の型(左辺値/右辺値)のまま転送します。
▶ サンプル:std::forwardで完全転送(難易度 ⭐⭐⭐)
#include <iostream>
#include <utility>
void process(int& x) {
std::cout << "左辺値参照..." << std::endl;
}
void process(int&& x) {
std::cout << "右辺値参照..." << std::endl;
}
template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg)); // 完全転送
}
int main() {
int x = 10;
wrapper(x); // 左辺値として転送
wrapper(20); // 右辺値として転送
return 0;
}
6. ムーブセマンティクスのパフォーマンス
▶ サンプル 1:コピーとムーブの比較(難易度 ⭐⭐)
#include <iostream>
#include <vector>
#include <chrono>
int main() {
std::vector<std::vector<int>> v;
// コピーの場合
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 10000; i++) {
std::vector<int> temp(1000, 1);
v.push_back(temp); // コピー!
}
auto end = std::chrono::high_resolution_clock::now();
auto copy_time = std::chrono::duration<double>(end - start).count();
v.clear();
// ムーブの場合
start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 10000; i++) {
std::vector<int> temp(1000, 1);
v.push_back(std::move(temp)); // ムーブ!
}
end = std::chrono::high_resolution_clock::now();
auto move_time = std::chrono::duration<double>(end - start).count();
std::cout << "コピー時間:" << copy_time << " 秒" << std::endl;
std::cout << "ムーブ時間:" << move_time << " 秒" << std::endl;
return 0;
}
▶ サンプル 3:ムーブ専用型(難易度 ⭐⭐⭐)
#include <iostream>
#include <utility>
class Resource {
private:
int* data;
size_t size;
public:
Resource(size_t n) : size(n), data(new int[n]) {
std::cout << n << " 要素を確保しました" << std::endl;
}
// ムーブコンストラクタ
Resource(Resource&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
std::cout << "リソースをムーブしました" << std::endl;
}
// コピーコンストラクタを削除
Resource(const Resource&) = delete;
Resource& operator=(const Resource&) = delete;
~Resource() {
if (data) {
delete[] data;
std::cout << size << " 要素を解放しました" << std::endl;
}
}
};
int main() {
Resource r1(100);
Resource r2 = std::move(r1);
return 0;
}
出力:
TEXT 📖 参照専用100 要素を確保しました リソースをムーブしました 100 要素を解放しました
❓ よくある質問
Q:std::moveは必ずムーブする? A: いいえ。型がムーブコンストラクタを持たない場合、コピーコンストラクタが呼ばれます(ムーブできない)。
Q:ムーブ後のオブジェクトは? A:ムーブ後のオブジェクトは有効だが未規定の状態になります。値は保証されませんが、破棄は可能です。
Q:いつムーブセマンティクスを使う? A: - 大きなオブジェクトを返す場合(関数の戻り値)
- 一時オブジェクトから所有権を移転する場合(
std::move)- コンテナへの要素追加(
emplace_back)
📖 まとめ
| トピック | 要点 |
|---|---|
| 右辺値参照 | && で右辺値にバインド |
| std::move | 左辺値を右辺値参照にキャスト |
| ムーブコンストラクタ | リソースを横取り、高速 |
| ムーブ代入 | ムーブ代入 |
| 完全転送 | std::forward で元の型のまま転送 |
📝 練習問題
-
初級(難易度 ⭐):
vector<string>を作成し、push_backで文字列を追加し、出力を観察。次にemplace_backを使用。 -
中級(難易度 ⭐⭐):
MyStringクラスを実装(コピーコンストラクタとムーブコンストラクタ含む)、main関数でstd::moveでムーブセマンティクスを試し、コンストラクタの呼び出しを観察。 -
上級(難易度 ⭐⭐⭐): ムーブ専用型(
unique_ptrのような)を実装し、コピーコンストラクタとコピー代入を削除し、ムーブコンストラクタとムーブ代入を使用。テストコードを書く。
- 左辺値:名前がありアドレスを持つ、永続的な存在
- 右辺値:一時オブジェクト、アドレスを持たない
std::moveは左辺値を右辺値参照にキャスト- ムーブコンストラクタ:リソースの所有権を移転
- ムーブセマンティクスはパフォーマンスを向上、ディープコピーを回避
次のレッスン:スマートポインタ応用(#46)