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)」。
-
左辺値を渡す →
Tはint&、argは左辺値 -
右辺値を渡す →
Tはint、argは 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