🌟

C++11 が左辺値と右辺値を5分類に拡張した理由

に公開

はじめに

C++11 で「右辺値参照(rvalue reference)」が導入されたことで、C++ の値分類(value category)は大きく進化しました。
C++98 では「左辺値(lvalue)」と「右辺値(rvalue)」の二分法しかありませんでしたが、C++11 ではさらに細かく分類されます:

C++11で導入された新しい概念である
xvalue(期限切れ右辺値)prvalue(純粋右辺値) が、
この章の中核的な進化ポイントです。


C++98 時代の「単純な世界」

C++98 の時代はとてもシンプルでした。

  • 左辺値 (lvalue):代入の左側に置けるもの。永続的なアドレスを持つ。
    例:a, *ptr, array[i]

  • 右辺値 (rvalue):一時的な値。すぐに破棄される。
    例:a + 5, 42, func()

ルールも直感的でした:

int a = 3;
&a;        // OK 左辺値はアドレスが取れる
&(a + 5);  // エラー!右辺値はアドレスが取れない

しかしこの単純さが、性能の壁 になっていました。


問題:無駄なコピーが多すぎる

std::vector<int> create_vector() {
    std::vector<int> temp;
    temp.push_back(1);
    temp.push_back(2);
    temp.push_back(3);
    return temp;  // ここで何が起こる?
}

std::vector<int> v = create_vector();

C++98 の仕様では return temp;コピーが発生 します。
temp のヒープメモリをまるごと複製し、呼び出し側に返すのです。

RVO(Return Value Optimization) により最適化できる場合もありますが、
あくまでコンパイラ依存で、保証はありません。


委員会の発想:「コピーせずに“盗む”ことはできないか?」

2000年代初期、C++標準化委員会が考えました。

「どうせこの一時オブジェクトはすぐ消える。なら資源をコピーせず盗めないか?

つまりこうです:

std::vector<int> v = create_vector();

このとき、create_vector() の戻り値が「もうすぐ死ぬ」ことを言語レベルで認識できれば、
ヒープの所有権をそのまま v に移動 させることができます。
これが「ムーブセマンティクス(move semantics)」の誕生です。


でも、どのオブジェクトが「盗める」の?

単純に「右辺値なら盗める」とすると、こんな問題が出ます。

std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = v1; // v1 は右辺値?→違う!

v1名前を持つ オブジェクト。つまり左辺値。
「まだ使うかもしれない」ので、資源を盗んではいけません。

ではどうするか?


std::move の登場と「期限切れ右辺値 (xvalue)」

std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1);

std::move(v1) は、左辺値を「盗んでもいい状態」に変換 します。

  • 構文的には左辺値(名前を持つ)

  • しかし意味的には「もう使わない」

この「見た目は左辺値、意味は右辺値」な存在こそが、
C++11 最大の革新―― xvalue(期限切れ右辺値) です。


5分類の整理

種類 名称 特徴
lvalue 左辺値 a, *ptr, obj.member 名前・アドレスあり。盗めない
prvalue 純粋右辺値 42, a + b, create_vector() 一時的。アドレスなし。盗める
xvalue 期限切れ右辺値 std::move(v) アドレスあり。資源を移動できる
glvalue 一般化左辺値 lvalue + xvalue の総称 アドレスを持つ式
rvalue 右辺値 prvalue + xvalue の総称 資源を移動できる式

つまり実体としては3つ:
lvalue / prvalue / xvalue
残り2つは分類上の集合にすぎません。


なぜここまで複雑にしたのか?

理由はひとつ。

“移動可能かつ識別可能” なオブジェクトを正しく表現するため。

例を見てみましょう:

std::vector<int> create_vector() {
    std::vector<int> temp = {1, 2, 3};
    return temp;
}

auto&& ref = create_vector();
std::cout << ref.size() << std::endl;

create_vector() の戻り値は一見右辺値(rvalue)ですが、
ref に束縛されて寿命が延長されています。
これは「アドレスがあり、かつ移動可能」。
すなわち xvalue(期限切れ右辺値) です。


これにより可能になったこと

移動コンストラクタ / 移動代入

std::vector<int> v2 = std::move(v1); // move ctor
v2 = std::move(v3); // move assignment

値の種類によって、自動的に「コピー」か「ムーブ」が選ばれます。


完全転送(Perfect Forwarding)

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

この T&& は「右辺値参照」ではなく「転送参照(forwarding reference)」。

  • 左辺値を渡す → Tint&arg は左辺値

  • 右辺値を渡す → Tintarg は xvalue

std::forward が、元の値カテゴリを保持して転送します。
これも xvalue が存在するおかげです。


他言語との比較

言語 解決アプローチ 特徴
Rust 所有権システム 所有権が自動移動。std::move 不要
Java/C# 参照型+GC コピーもムーブも不要。性能コストあり
Python 全て参照渡し コピー概念なし。副作用リスクあり
C++ 値と資源の制御を開発者に委ねる ゼロオーバーヘッドを追求

C++ は「全て自分で管理する」という哲学を貫いています。
それが難しさであり、強さでもあります。


まとめ:覚えておきたい実戦ポイント

  • 関数からローカルを返すときはコピーされない(C++11以降自動でムーブ)

  • 資源を移したいときは std::move

  • テンプレート引数の転送には std::forward

  • lvalue = 名前を持つ

  • prvalue = 一時的な値

  • xvalue = “使い終わった” 左辺値


結論

C++11 の値カテゴリ体系は一見複雑ですが、
これを理解すれば「C++の現代的な型システムの本質」に到達します。

「C++ は、シンプルな問題を複雑にする最も上手い言語」
——しかし、その複雑さの裏には“ハードウェアの限界を引き出す力”がある。

これは呪いであり、同時に誇りでもあります。

Discussion