ムーブセマンティクスと右辺値
CoddyのC++ジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 83/104。
C++において、すべての式はlvalue(永続的なアイデンティティを持ち、アドレスを取得できる)またはrvalue(一時的で、破棄される直前のもの)のいずれかです。この区別を理解することで、不必要なコピーを回避する強力な最適化手法であるムーブセマンティクスを活用できるようになります。
&& で宣言される右辺値参照は、一時オブジェクトに特化してバインドします。これにより、どのみち消滅するオブジェクトからリソースを「盗む」ことが可能になります:
#include <iostream>
#include <utility>
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t s) : data(new int[s]), size(s) {
std::cout << "Constructed\n";
}
// ムーブコンストラクタ - リソースを奪う
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr; // ソースを有効な状態にする
other.size = 0;
std::cout << "Moved\n";
}
~Buffer() { delete[] data; }
};
int main() {
Buffer b1(1000);
Buffer b2(std::move(b1)); // ムーブコンストラクタをトリガーする
}std::move関数は実際には何も移動しません。単に左辺値を右辺値参照にキャストし、そのオブジェクトのリソースを放棄してもよいという合図を送るだけです。実際の移動は、ムーブコンストラクタまたはムーブ代入演算子の中で行われます。
ムーブセマンティクスは、コンテナや文字列のようなリソースを大量に消費するオブジェクトを扱う際、パフォーマンスを劇的に向上させます。何メガバイトものデータをディープコピーする代わりに、単にポインタの所有権を移譲します。これは、サイズに関係なく定数時間で行われる操作です。
チャレンジ
簡単ムーブセマンティクスの動作を実証する、リソース管理を行う DataBuffer クラスを構築しましょう。動的に割り当てられたメモリの所有権を移譲する際、コピーする代わりにムーブすることで、効率が劇的に向上することを確認できます。
コードは以下の3つのファイルに分けて構成します:
DataBuffer.h: 整数型の動的配列を管理するDataBufferクラスを定義します。クラスには、データポインタ (
int*)、サイズ (size_t)、および操作中にどのバッファであるかを追跡するための名前 (std::string) をプライベートメンバとして持たせる必要があります。以下を宣言してください:
std::stringの名前とsize_tのサイズを受け取り、配列を割り当て、[name] constructed with size [size]と出力するコンストラクタ- 右辺値参照を受け取り、リソースを奪い、
[name] moved from [source_name]と出力するムーブコンストラクタ(ムーブ先のバッファはソースの名前を引き継ぎます) [name] destroyed(バッファがムーブされた後の場合はempty destroyed) と出力するデストラクタ- 現在のサイズを返す
getSize()メソッド - バッファの名前を返す
getName()メソッド
ムーブコンストラクタには
noexceptを指定し、ソースオブジェクトを有効な空の状態(nullptr、サイズ 0、名前 "empty")にすることを忘れないでください。DataBuffer.cpp: ヘッダーで宣言したすべてのメソッドを実装します。デストラクタを実行する際は、ポインタが null でない場合にのみデータを削除してください。出力のために<iostream>をインクルードします。main.cpp: 2つの入力を読み取ります:- バッファの名前 (string)
- バッファのサイズ (integer)
指定された名前とサイズで
DataBufferを作成します。次に、std::move()を使用して最初のバッファからムーブすることで、2つ目のバッファを作成します。ムーブ後、両方のバッファの状態を出力します:Original: [name] size=[size]New: [name] size=[size]
std::moveのために<utility>をインクルードしてください。
例えば、入力が Alpha と 100 の場合:
Alpha constructed with size 100
Alpha moved from Alpha
Original: empty size=0
New: Alpha size=100
Alpha destroyed
empty destroyed入力が Buffer と 50 の場合:
Buffer constructed with size 50
Buffer moved from Buffer
Original: empty size=0
New: Buffer size=50
Buffer destroyed
empty destroyedムーブコンストラクタが、データをコピーすることなく割り当てられたメモリの所有権をどのように移譲するかに注目してください。元のバッファは空ですが有効な状態のままになり、プログラムの終了時に両方のバッファが破棄される際、メモリを所有し続けているバッファだけが実際にメモリを削除します。
チートシート
C++の式は、左辺値(lvalue)(永続的でアドレス指定可能)または右辺値(rvalue)(一時的)のいずれかです。ムーブセマンティクスは、一時的なオブジェクトからリソースをコピーするのではなく、転送することでパフォーマンスを最適化します。
右辺値参照
&& で宣言される右辺値参照は、一時的なオブジェクトにバインドします:
Buffer(Buffer&& other) // Rvalue reference parameterムーブコンストラクタ
一時的なオブジェクトからリソースを奪います。noexcept を指定し、ソース(移動元)を有効な空の状態にします:
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr; // Leave source valid
other.size = 0;
}std::move
左辺値を右辺値参照にキャストし、リソースを転送する意思があることを示します。<utility> をインクルードしてください:
Buffer b1(1000);
Buffer b2(std::move(b1)); // Triggers move constructorstd::move 自体は何も移動しません。実際の転送は、ムーブコンストラクタまたはムーブ代入演算子の中で行われます。
利点
ムーブセマンティクスは、オブジェクトのサイズに関係なく一定時間でリソース転送を可能にし、大規模なデータ構造の高コストなディープコピーを回避します。
自分で試してみよう
#include <iostream>
#include <string>
#include <utility>
#include "DataBuffer.h"
int main() {
std::string name;
int size;
std::cin >> name;
std::cin >> size;
// TODO: 指定された name と size で DataBuffer を作成する
// TODO: std::move() を使用して、1つ目のバッファからムーブして2つ目のバッファを作成する
// TODO: 両方のバッファの状態を出力する:
// Original: [name] size=[size]
// New: [name] size=[size]
return 0;
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
3コンストラクタとデストラクタ
デフォルトコンストラクタ引数付きコンストラクタコピーコンストラクタムーブコンストラクタコンストラクタ初期化リスト委譲コンストラクタデストラクタ詳解Rule of Three / Five / Zeroまとめ - Stringクラス6演算子オーバーロード
演算子オーバーロード入門算術演算子のオーバーロード比較演算子のオーバーロードストリーム演算子代入演算子のオーバーロード[] と () 演算子のオーバーロード型変換演算子まとめ:Matrix クラス12モダンC++の機能
ムーブセマンティクスと右辺値完全転送ラムダ式の詳細std::functionとstd::bindconstexprとconsteval構造化束縛optional, variant, any