ムーブセマンティクスと右辺値
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 関数は実際には何も移動させません。単に lvalue を rvalue 参照にキャストし、そのオブジェクトのリソースを放棄する意思があることを示すだけです。実際の移動は、ムーブコンストラクタまたはムーブ代入演算子の中で行われます。
ムーブセマンティクスは、コンテナや文字列のようなリソースを大量に消費するオブジェクトを扱う際に、パフォーマンスを劇的に向上させます。何メガバイトものデータをディープコピーする代わりに、単にポインタの所有権を移譲します。これは、サイズに関係なく定数時間の操作です。
チャレンジ
簡単ムーブセマンティクスの動作を実証する、リソース管理を行う 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, size 0, name "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ムーブコンストラクタが、データをコピーすることなく割り当てられたメモリの所有権を移譲していることに注目してください。元のバッファは空ですが有効な状態のまま残され、プログラムの終了時に両方のバッファが破棄される際、メモリを所有し続けているバッファだけが実際にメモリを削除します。
自分で試してみよう
#include <iostream>
#include <string>
#include <utility>
#include "DataBuffer.h"
int main() {
std::string name;
int size;
std::cin >> name;
std::cin >> size;
// TODO: 指定された名前とサイズで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