
2026/01/09 18:01
C++ std::move は何も移動しない:値カテゴリに関する詳細な検証
RSS: https://news.ycombinator.com/rss
要約▶
日本語訳:
以下は、C++ のムーブセマンティクスに関する要点を整理した改訂版です。元の意味や構造(見出し・箇条書き)を保持しつつ、日本語へ翻訳しました。
改訂概要
この記事では、C++ のムーブセマンティクスがどのように機能し、実際に効率的なムーブにつながる状況について説明しています。
- ベクトル再割り当て:
は要素型のムーブコンストラクタがstd::vector
と宣言されている場合のみ、再割り当て時に要素をムーブします。そうでないと、強い例外保証を保つためにコピーします。noexcept
: これは lvalue を rvalue リファレンス (std::move
) にキャストするだけです。T&&
自体がデータを移動させるわけではなく、ムーブコンストラクタ/代入演算子が呼び出されたときに実際のムーブが発生します。std::move- 戻り値: 名前付きローカル変数を返すと NRVO(Named Return Value Optimization)や自動ムーブが適用され、コピーが排除できる場合があります。
を明示的に適用するとこれらの最適化が防止され、パフォーマンス低下につながります。std::move - const オブジェクト:
オブジェクトからムーブするとconst
が得られます。const 修飾付き型ではコピーコンストラクタが呼び出されるため、実質的にコピーが行われ、コンパイラ警告は発生しません。const T&& - ムーブ後の状態: ソースオブジェクトは有効ですが値は未定義です。代入または破棄以外で使用すべきではありません。
- Rule of Five(五法則): デストラクタ、コピー/ムーブコンストラクタ、コピー/ムーブ代入演算子のいずれかを定義した場合、他の四つも実装する必要があります。そうしないと浅いコピーや二重解放が発生します。
: ムーブ実装ではstd::exchange
を使ってリソースを安全にスワップし、ソースを空状態に保ちます。std::exchange- 小オブジェクト:
が付与されていても、小さなオブジェクト(例:SSO を使用する小文字列)はインラインでデータが保持されるため、ムーブではなくコピーされることがあります。ムーブが必ずしも「無料」ではありません。noexcept - ループ/レンジ: ループ内で const リファレンスを使うと移動が行われません。所有権を効率的に転送するには非 const リファレンスを使用し、
を適用します。std::move - 継承: 派生クラスのムーブコンストラクタは基底部を明示的にフォワード (
) する必要があります。named rvalue リファレンスは lvalue として扱われるためです。Base(std::move(other)) - C++17 の保証: prvalue に対する強制コピー除去と、任意だが広く実行される NRVO は、ローカル変数の返却を通常効率的にします。
ムーブ操作を
noexcept としてマークすることは不可欠です。そうしないとコンテナは再割り当て時にコピーへフォールバックし、パフォーマンスが大幅に低下します。特に const オブジェクトやループでの誤用による std::move の乱用は不要なコピーを発生させ、効率を落とす原因となります。Rule of Five の正しい実装と std::exchange などのヘルパーを慎重に使用することで、リソース管理に関わるバグを防止できます。
欠落している項目: 5, 7‑11
推論 / 跳躍: NRVO を無効化する点について軽微な言い換えのみ。全体の要旨は元のポイントに忠実です。
本文
問題点:最適化が逆に処理を遅くしてしまうケース
まず、経験豊富な開発者でもつまづきやすいポイントから始めましょう。以下のコードは一見合理的に見えるC++です。
struct HeavyObject { std::string data; HeavyObject(HeavyObject&& other) : data(std::move(other.data)) {} HeavyObject(const HeavyObject& other) : data(other.data) {} HeavyObject(const char* s) : data(s) {} }; std::vector<HeavyObject> createData() { std::vector<HeavyObject> data; // … データを埋める … return data; } void processData() { auto result = createData(); }
このコードは動作します。コンパイルも通り、実行時に問題が生じるわけではありません。しかし、型の実装次第でコピー操作が数千回発生し、ムーブよりずっと重い処理になってしまう可能性があります。
実際に起きていることはこうです。
std::vector が保持している容量を超えると、新しいメモリ領域へ再配置(リアロケーション)します。この時点で既存の要素をすべて新しい場所へ「ムーブ」するか「コピー」するかが決まります。もしムーブコンストラクタに noexcept が付いていない場合、コンパイラはムーブではなくコピーを選択します。なぜなら、std::vector は 強力な例外保証(strong exception guarantee)を保つ必要があるからです。
- コピー中に例外が投げられた場合、元のベクターは破損せずそのまま残ります。
- ムーブ中に例外が投げられると、一部の要素だけムーブ済みになり、元のベクターが壊れてしまう恐れがあります。
したがって、ムーブコンストラクタを
noexcept と明示しない限り、コンテナは安全性を優先してコピーします。
1. 実際に std::move
は何をするのか?
std::movetemplate<typename _Tp> constexpr typename std::remove_reference<_Tp>::type&& move(_Tp&& __t) noexcept { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }
std::move は「ムーブ」そのものを行うわけではなく、値カテゴリ(value category) を変更するキャストにすぎません。lvalue → xvalue に変換しているだけです。実際のデータ移動は、その xvalue でムーブコンストラクタやムーブ代入演算子が呼ばれた時点で起こります。
2. 値カテゴリを理解する
| カテゴリ | 説明 |
|---|---|
| lvalue | アイデンティティを持つ値。アドレスを取れる。例: |
| prvalue | 純粋な rvalue。一時オブジェクトでアイデンティティがない。例:, , |
| xvalue | 終わりに近い値。まだアイデンティティはあるが、破棄される直前。 が作るもの。 |
| glvalue | lvalue と xvalue を合わせた一般化された lvalue。 |
std::move(name) と書くと、lvalue であった name を xvalue に変換します。実際のデータは移動しません。ただし、その式中では「ムーブ対象」として扱われます。
3. パフォーマンスを損なうよくあるミス
| ミス | コード例 | 問題点 |
|---|---|---|
返却時に を使う | | NRVO(Named Return Value Optimization)を阻害し、ムーブが発生。ムーブはコピーより遅いケースもある。 |
オブジェクトからムーブする | | は変更できないため、ムーブは不可能。コンパイラはコピーにフォールバック。 |
| ムーブ後のオブジェクトを使う | | ムーブされたオブジェクトは安全に再代入または破棄以外の操作を行わない。 |
4. ムーブセマンティクスを正しく実装する
Rule of Five
次の特殊メンバ関数を1つ定義したら、通常はすべて5つを用意します。
- デストラクタ
- コピーコンストラクタ
- コピー代入演算子
- ムーブコンストラクタ
- ムーブ代入演算子
class Resource { int* data; size_t size; public: Resource(size_t n) : data(new int[n]), size(n) {} ~Resource() { delete[] data; } // コピー操作 Resource(const Resource& other) : data(new int[other.size]), size(other.size) { std::copy(other.data, other.data + size, data); } Resource& operator=(const Resource& other) { if (this != &other) { int* new_data = new int[other.size]; std::copy(other.data, other.data + other.size, new_data); delete[] data; data = new_data; size = other.size; } return *this; } // ムーブ操作(noexcept) Resource(Resource&& other) noexcept : data(std::exchange(other.data, nullptr)), size(std::exchange(other.size, 0)) {} Resource& operator=(Resource&& other) noexcept { if (this != &other) { delete[] data; data = std::exchange(other.data, nullptr); size = std::exchange(other.size, 0); } return *this; } };
ポイント
を使って、ムーブ元のリソースを安全に「空」にしておく。std::exchange- ムーブ操作は必ず
にする。そうしないとコンテナがコピーに戻る。noexcept
5. std::move
と std::forward
の使い分け
std::movestd::forward| シチュエーション | 推奨関数 |
|---|---|
| オブジェクトの所有権を譲りたく、以降は使用しない | |
| テンプレート内で引数の元の値カテゴリ(lvalue / rvalue)を保ちたい | |
6. 実務チェックリスト
- 返却時に
を使わない。NRVO や暗黙ムーブが最適化。std::move - ムーブコンストラクタ/代入演算子は 必ず
にする。noexcept
オブジェクトに対してはconst
を呼ばない。std::move- ムーブ後のオブジェクトは「再代入」か「破棄」以外には使わない。
- テンプレートで完璧転送が必要な場合のみ
を使用。std::forward
7. 実例:ムーブ対応動的配列
template<typename T> class DynamicArray { T* data_; size_t size_, capacity_; void reserve_more() { size_t new_cap = capacity_ == 0 ? 1 : capacity_ * 2; T* new_data = new T[new_cap]; for (size_t i = 0; i < size_; ++i) new_data[i] = std::move(data_[i]); // 要素をムーブ delete[] data_; data_ = new_data; capacity_ = new_cap; } public: DynamicArray() : data_(nullptr), size_(0), capacity_(0) {} ~DynamicArray() { delete[] data_; } // コピー操作 DynamicArray(const DynamicArray& other) : data_(new T[other.capacity_]), size_(other.size_), capacity_(other.capacity_) { std::copy(other.data_, other.data_ + size_, data_); } DynamicArray& operator=(const DynamicArray& other) { if (this != &other) { T* new_data = new T[other.capacity_]; std::copy(other.data_, other.data_ + other.size_, new_data); delete[] data_; data_ = new_data; size_ = other.size_; capacity_ = other.capacity_; } return *this; } // ムーブ操作(noexcept) DynamicArray(DynamicArray&& other) noexcept : data_(std::exchange(other.data_, nullptr)), size_(std::exchange(other.size_, 0)), capacity_(std::exchange(other.capacity_, 0)) {} DynamicArray& operator=(DynamicArray&& other) noexcept { if (this != &other) { delete[] data_; data_ = std::exchange(other.data_, nullptr); size_ = std::exchange(other.size_, 0); capacity_ = std::exchange(other.capacity_, 0); } return *this; } void push_back(const T& v) { if (size_==capacity_) reserve_more(); data_[size_++] = v; } void push_back(T&& v) { if (size_==capacity_) reserve_more(); data_[size_++] = std::move(v); } // … アクセサ、size()、capacity() など … };
このクラスは Rule of Five を守り、ムーブ操作を
noexcept に設定し、再配置時に要素を効率的にムーブしています。
8. まとめ
はキャストで値カテゴリを変えるだけ。実際のデータ移動は後続のムーブコンストラクタ/代入演算子が行う。std::move- 不要な
を避ける。NRVO や暗黙ムーブに任せれば高速で安全。std::move - ムーブ操作を必ず
にする。そうしないとコンテナはコピーへフォールバック。noexcept
オブジェクトからはムーブできない。コピーが発生するので注意。const- ムーブ後のオブジェクトは「再代入」か「破棄」のみで扱う。
これらを頭に入れれば、ムーブセマンティクスを正しく活用しつつ、高速で安全な C++ コードを書けるようになります。