Menu
Coddy logo textTech

ムーブセマンティクスと右辺値

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 参照にキャストし、そのオブジェクトのリソースを放棄する意思があることを示すだけです。実際の移動は、ムーブコンストラクタまたはムーブ代入演算子の中で行われます。

ムーブセマンティクスは、コンテナや文字列のようなリソースを大量に消費するオブジェクトを扱う際に、パフォーマンスを劇的に向上させます。何メガバイトものデータをディープコピーする代わりに、単にポインタの所有権を移譲します。これは、サイズに関係なく定数時間の操作です。

challenge icon

チャレンジ

簡単

ムーブセマンティクスの動作を実証する、リソース管理を行う 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つの入力を読み取ります:
    1. バッファの名前 (string)
    2. バッファのサイズ (integer)

    指定された名前とサイズで DataBuffer を作成します。次に、std::move() を使用して最初のバッファからムーブすることで、2つ目のバッファを作成します。ムーブ後、両方のバッファの状態を出力します:

    • Original: [name] size=[size]
    • New: [name] size=[size]

    std::move のために <utility> をインクルードしてください。

例えば、入力が Alpha100 の場合:

Alpha constructed with size 100
Alpha moved from Alpha
Original: empty size=0
New: Alpha size=100
Alpha destroyed
empty destroyed

入力が Buffer50 の場合:

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;
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン