はじめに
C++11で導入されたムーブセマンティクスは、不要なコピーを避けてパフォーマンスを劇的に改善する機能。
実測結果を見てほしい。無限倍だよ?
実測結果:
Copy time: 41568 us
Move time: 0 us
Speedup: ∞x
コピーとムーブの違い
コピー
std::vector<int> v1(10000, 42);
std::vector<int> v2 = v1; // コピー
// v1とv2は別々のメモリを持つ
// 10000要素分のメモリ確保 + コピー
ムーブ
std::vector<int> v1(10000, 42);
std::vector<int> v2 = std::move(v1); // ムーブ
// v2はv1のメモリを「奪う」
// v1は空になる(有効だが未規定の状態)
// メモリ確保もコピーもなし
std::move
std::moveはrvalue参照にキャストするだけで、実際にムーブするわけではありません。
std::string s1 = "Hello";
std::string s2 = std::move(s1); // s1をrvalue参照にキャスト → ムーブコンストラクタが呼ばれる
std::cout << s1; // "" (空、有効だが未規定)
std::cout << s2; // "Hello"
// ムーブ後も再代入は安全
s1 = "New value";
ムーブコンストラクタの実装
class Buffer {
private:
int* data;
size_t size;
public:
// ムーブコンストラクタ
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size)
{
other.data = nullptr; // 所有権を奪う
other.size = 0;
}
// ムーブ代入演算子
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data; // 既存リソースを解放
data = other.data; // 所有権を奪う
size = other.size;
other.data = nullptr;
other.size = 0;
}
return *this;
}
};
Rule of Five
5つの特殊メンバ関数のいずれかを定義したら、全て定義すべきです。
class MyClass {
public:
~MyClass(); // 1. デストラクタ
MyClass(const MyClass&); // 2. コピーコンストラクタ
MyClass& operator=(const MyClass&); // 3. コピー代入演算子
MyClass(MyClass&&) noexcept; // 4. ムーブコンストラクタ
MyClass& operator=(MyClass&&) noexcept; // 5. ムーブ代入演算子
};
不要なら明示的に:
class NonCopyable {
public:
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
NonCopyable(NonCopyable&&) = default;
NonCopyable& operator=(NonCopyable&&) = default;
};
noexcept の重要性
ムーブコンストラクタには必ず noexceptを付けましょう。
// ✓ 良い
Buffer(Buffer&&) noexcept;
// ❌ 悪い
Buffer(Buffer&&); // noexceptなし
理由: std::vectorの再アロケーション時、ムーブコンストラクタがnoexceptでないと、例外安全性のためにコピーが使われてしまいます。
Perfect Forwarding
テンプレート関数でlvalue/rvalueを正しく転送するにはstd::forwardを使います。
void process(int& x) { std::cout << "lvalue\n"; }
void process(int&& x) { std::cout << "rvalue\n"; }
template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg)); // 完全転送
}
int x = 42;
wrapper(x); // lvalue → process(int&)
wrapper(100); // rvalue → process(int&&)
wrapper(std::move(x)); // rvalue → process(int&&)
vectorでの活用
std::vector<std::string> vec;
std::string s = "Hello";
// コピー
vec.push_back(s); // sはそのまま
std::cout << s; // "Hello"
// ムーブ
vec.push_back(std::move(s)); // sは空になる
std::cout << s; // "" (空)
// 直接構築(emplace_back)
vec.emplace_back("World"); // コピーもムーブもなし
関数の戻り値
std::vector<int> createVector() {
std::vector<int> v(10000);
return v; // RVOまたはムーブ
}
auto v = createVector(); // コピーは発生しない
RVO (Return Value Optimization) により、多くの場合コピーもムーブも省略されます。
注意: return std::move(v);と書く必要はありません。逆にRVOが無効化される可能性があります。
いつムーブを使うか
✓ 使うべき場面
// 1. 不要になったオブジェクト
std::string result = std::move(temp);
// 2. コンテナへの挿入
vec.push_back(std::move(obj));
// 3. 所有権の移動
std::unique_ptr<T> p2 = std::move(p1);
✗ 使うべきでない場面
// 1. 後で使うオブジェクト
std::move(important_data); // ダメ!
// 2. const オブジェクト
const std::string s = "Hello";
std::move(s); // コピーになる(constなのでムーブできない)
// 3. 関数のreturn
return std::move(local); // 不要、RVOを阻害する
パフォーマンス実測
const int N = 100000;
std::vector<std::string> source(N, std::string(1000, 'x'));
// コピー
std::vector<std::string> copy = source;
// → 41,568 μs
// ムーブ
std::vector<std::string> moved = std::move(source);
// → 0 μs (ほぼ瞬時)
100MBのデータがムーブなら**ポインタのコピー(数バイト)**で済みます。
まとめ
| 概念 | 説明 |
|---|---|
| ムーブ | リソースの所有権を移動 |
std::move |
rvalue参照へのキャスト |
&& |
rvalue参照 |
noexcept |
ムーブに必須 |
std::forward |
完全転送 |
| RVO | 戻り値の最適化 |
ポイント:
- 不要になったオブジェクトは
std::move - ムーブコンストラクタには
noexcept -
returnでstd::moveは不要 - ムーブ後のオブジェクトは使わない
// 基本パターン
std::string s = "Hello";
vec.push_back(std::move(s)); // sはもう使わない