C++の左辺値と右辺値:値カテゴリとリソース移動の仕組み

値カテゴリの基礎定義

C++において、式の性質はライフサイクルとメモリ識別の観点から分類されます。以下の表は各カテゴリの特性を比較したものです。

カテゴリ左辺値左辺値参照右辺値右辺値参照
識別性永続的な名前を持ち、&演算子でアドレス取得が可能既存オブジェクトに対する別名一時的な結果またはリテラル。アドレス取得不可一時オブジェクト、または明示的に右辺値化された識別子への参照
主な役割状態の保持、代入演算の対象関数引数への効率化、オブジェクトの直接操作定数値の伝播、演算結果の受け渡し所有権の移動(ムーブ)、テンプレート引数の属性維持

左辺値の判定基準

メモリ上で明確なアドレスを保持し、複数回参照可能な式は左辺値に分類されます。

変数および識別子

int counter = 42; // 変数counter自体は左辺値

左辺値参照を戻り値とする関数呼び出し

std::string& fetchBuffer(); // 呼び出し結果は左辺値

前置インクリメント/デクリメント

前置演算子は操作対象のオブジェクト自身を返す仕様となっているため、代入の左側へ配置可能です。

int idx = 0;
++idx;
++idx = 15; // 合法:更新後のオブジェクトへ直接代入

代入および複合代入演算

C++の代入演算子は左辺値参照を返すため、演算の連鎖が可能です。

int x = 0;
(x += 5) = 10; // 5加算後、結果に対して再度10を代入

ポインタ逆参照

struct Node { int val; };
Node* ptr = new Node{1};
*ptr; // 逆参照されたオブジェクトは左辺値

右辺値の分類:prvalueとxvalue

C++11以降、右辺値は生成プロセスと所有権の扱いに応じて二つに細分化されています。

純粋右辺値(prvalue)

リテラルや、値を返す関数の結果など、物理的な実体がまだ確定していない一時的な値です。

int score = 85; // リテラル85はprvalue

struct Engine {
    int rpm() { return 7000; }
};
Engine motor;
motor.rpm(); // 値を返す呼び出し結果はprvalue

後置インクリメントは増加前の一時コピーを返すため、prvalueとして扱われ、代入は禁止されています。

int cnt = 0;
cnt++;
cnt++ = 9; // コンパイルエラー:非修飾右辺値への代入は不可

算術演算や比較演算の結果も同様にprvalueです。

int a = 3, b = 4;
a * b;    // 演算結果
a != b;   // 真偽値結果

消滅値(xvalue)

消滅値は、リソースの移動が意図されている「寿命間近のオブジェクト」を指します。std::moveの適用結果や、右辺値参照を返す関数の戻り値が該当します。

class ResourceOwner {
public:
    ResourceOwner(ResourceOwner&& src) noexcept {
        std::cout << "Move Constructor triggered\n";
    }
    ResourceOwner& operator=(ResourceOwner&& src) noexcept {
        std::cout << "Move Assignment triggered\n";
        return *this;
    }
};

ResourceOwner acquire() {
    ResourceOwner temp;
    return temp;
}

消滅値が存在することで、コンパイラはコピーコンストラクタの代わりに移動コンストラクタを選択し、動的メモリやファイルハンドルなどの所有権を効率的に引き継ぎます。対象のオブジェクトは移動後に有効だが空の状態となり、デストラクタ実行時まで待機します。

参照の束縛と変換ルール

参照は宣言時に初期化が必要であり、再バインドは不可能です。

int&& rref = 100; // 右辺値参照の直接初期化

重要な仕様事項:

  • const左辺値参照は右辺値を束縛できますが、変更操作はコンパイラによって阻止されます。
  • std::moveを使用することで、左辺値を消滅値(xvalue)へキャストし、右辺値参照として扱わせることが可能です。
  • コード上で宣言された参照変数名(例:rref)自体は、アドレス取得が可能なため内部的には左辺値として処理されます。

ムーブセマンティクスによる効率化

深いコピーを必要とするリソース管理クラスにおいて、不要なメモリ確保・解放を回避する仕組みです。移動演算を実装することで達成されます。

class Buffer {
public:
    explicit Buffer(size_t len) : data_(new char[len]), size_(len) {}

    // 移動コンストラクタ
    Buffer(Buffer&& src) noexcept : data_(src.data_), size_(src.size_) {
        src.data_ = nullptr;
        src.size_ = 0;
    }

    // 移動代入演算子
    Buffer& operator=(Buffer&& src) noexcept {
        if (this != &src) {
            delete[] data_;
            data_ = src.data_;
            size_ = src.size_;
            src.data_ = nullptr;
            src.size_ = 0;
        }
        return *this;
    }

    ~Buffer() { delete[] data_; }
private:
    char* data_ = nullptr;
    size_t size_ = 0;
};

void usage() {
    Buffer origin(1024);
    Buffer copy(std::move(origin)); // 所有権の移動が発生
    // originはリソースを失い、空状態となる
}

STLコンテナも同様にprvalueやxvalueを受け取ると、自動的に移動演算を適用します。

std::deque<Buffer> queue;
queue.push_back(Buffer(512)); // 一時オブジェクトが渡され、移動コンストラクタが選択される

完全転送とテンプレート参照の振る舞い

テンプレート関数が受け取った引数の値カテゴリを維持したまま、下位の関数へ引き渡す技術です。

void execute(int& arg) {
    std::cout << "Lvalue processed\n";
}
void execute(int&& arg) {
    std::cout << "Rvalue processed\n";
}

template<typename T>
void forward_proxy(T&& arg) {
    execute(std::forward<T>(arg));
}

転送参照(旧ユニバーサル参照)の型推論

T&& 形式のテンプレートパラメータは、実引数のカテゴリによって推論結果が変化します。これはC++の参照の折りたたみ規則に基づいています。

  • 左辺値または左辺値参照が渡された場合:Tは U& に推論され、T&&U& へ折りたたまれます。
  • 右辺値または右辺値参照が渡された場合:Tは U に推論され、T&&U&& のまま維持されます。
int val = 20;
forward_proxy(val);              // 左辺値として転送
forward_proxy(std::move(val));   // 右辺値として転送
forward_proxy(30);               // リテラル右辺値として転送

std::forward<T> は推論済みのT型に基づきキャストを行います。Tが左辺値参照の場合、引数はそのまま左辺値として渡されます。Tが非参照または右辺値参照の場合、引数は static_cast を介して右辺値へ変換され、移動セマンティクスやオーバーロード解決が正しく機能するよう制御されます。

タグ: C++11 ムーブセマンティクス 右辺値参照 完全転送 値カテゴリ

6月27日 18:47 投稿