int a = 10;
という文は, a
という変数を用意し, それはint
型なのでメモリのどこかに4バイトの領域を確保し, そこに10
を意味するビット列を記述せよという命令です.
a
という記号は, a
という名前があり, メモリのどこかに実体を持ちます.
では, 10
とは何なのでしょうか? これはアドレスがあるわけではなく, a
を初期化するために一時的に導入した記号に過ぎません. a
と10
は全く異なる役割を持っていることが分かります.
今回はC++における「値カテゴリ (value category)」という概念について説明します. 用語は本来英語ですが, これらはソースコード上に書くようなものでもなく, 英語で説明するメリットが特にないので日本語で説明します.
値カテゴリ
C++において, 式 a = b
に登場する値やオブジェクトは次のようにカテゴライズされます.
- 一般左辺値 (glvalue)
- 左辺値 (lvalue)
- 移動値 (xvalue)
- 右辺値 (rvalue)
- 純粋右辺値 (prvalue)
- 移動値 (xvalue)
例えば,
int a = 10;
という文において, a
は左辺値, 10
は純粋右辺値です. 大雑把に言えば, 左辺値とは実体(i.e., 専用に確保されたメモリ領域)があるもの, 右辺値とは実体がないものと言えます.
問題となるのは移動値です. 正直, 移動値以外は何も難しいことはないので, 値カテゴリを理解するというのは移動値を理解することとほぼ同義です.
移動値: xvalue
まず, 上の分類では移動値が左右のどちらにも書いてありますが, 本来移動値は右辺値です. 話を簡単にするため, ひとまず右辺値としての移動値について説明します.
移動値とは, 元々別の名前とポインタを持った実体があり, その式によって値が左辺に「移動」するオブジェクトのことを言います. これをムーブセマンティクスといい, 移動値とはムーブセマンティクスを誘発するオブジェクトと定義されます. (メモリ上を「移動」するというように, プログラムの挙動をある程度曖昧に表現したものを「セマンティクス(意味論)」と言います. この記事もC++を意味論的に解説していると言えますね.)
ところで, 式 a=10
が無くても10
という記号は常に純粋右辺値と言えるのと同じように, あるオブジェクトb
が移動値であるというのは, 移動を伴う式が無くても成り立つb
自体の性質です. 移動値は次のようにして生成できます.
- 実体を伴うオブジェクト
B
に対し,std::move(B)
- 移動値オブジェクトのメンバ
std::move(B).member
- 型
T
の右辺値参照を返す関数T&& func()
の戻り値
#include <iostream>
#include <utility>
int main(void){
class MyClass{
public:
std::string member = "hello";
};
MyClass a = MyClass();
MyClass b = std::move(a);
}
このとき, std::move(a)
は移動値です. このとき何が起きるのか, つまりC++におけるムーブセマンティクスとは何かという説明については次回にしたいと思います. 今回は値カテゴリの話なので.
右辺値参照型
値カテゴリを見ると, 移動値は一般左辺値にも含まれていることが分かります. これが何を意味するのかというと, 移動値は実体を持ちます.
#include <iostream>
#include <utility>
int main(void){
class MyClass{
public:
std::string member = "hello";
};
MyClass a = MyClass();
std::cout << &a << std::endl;
MyClass&& x = std::move(a);
std::cout << &x << std::endl;
}
0xffffe45298d0
0xffffe45298d0
この MyClass&&
をMyClass
の右辺値参照型と言います. これに対し, 通常のMyClass&
を左辺値参照型といいます. これらは異なる型として扱われます.
純粋右辺値10
のポインタとして&(10)
のような書き方はできないので, 移動値が純粋右辺値とは異なる挙動をしていることがわかると思います.
ところで, 移動値が実体を持つということは, 移動値が左辺に来るこということなのでしょうか? この問いの答えはちょっとややこしいです. 多くの場合, 移動値 = ***
のような式は機能しません. そういう意味では移動値は右辺値です. しかし, ムーブ代入演算子の定義をオーバーライドすることで, 移動値を左辺に持ってくることができます. これはちょっと難しいしあまり使わない機能なので, ここでは割愛します. とりあえず, 特殊なことをすれば移動値は左辺にも来ることがあるので, 移動値は両方のカテゴリに存在しているということを理解すれば十分だと思います.
参照折り畳み規則と完全転送
(左辺値)参照型T&
の他に, 右辺値参照型T&&
が登場しました. このとき, 参照型の参照型は何型なのでしょうか? C++では参照の参照という型はなく, 参照の参照を書くと一定の規則に従って参照として解釈されます.
参照の参照は以下の規則によって折り畳まれます:
-
T& &
→T&
-
T& &&
→T&
-
T&& &
→T&
-
T&& &&
→T&&
ちょうど論理和に似ています. &
を真, &&
を偽とすれば論理和そのものですね.
この規則によって, 左辺値として渡された値は左辺値のまま, 右辺値として渡された値は右辺値のまま, 関数に渡されます. これを完全転送と言います.