Chapter 06

もうちょっとだけ細かい、だけどざっくりした話

dec9ue
dec9ue
2021.12.18に更新

Modern C++ の空気感が伝わってきたかな、というところで、なんとなくモヤモヤしてしまうキーワードについてもうほんの少しだけ、掘り下げてみます。

右辺値参照

右辺値参照をどう捉えるか、というのは本当にとらえどころのない話だと思いますが、普段の使い方としてはやはり、「関数引数におけるマッチングの用途に使う」と考えておくのが妥当そうです。

「右辺値参照」という言葉自体は「右辺値を束縛し、生存期間を延長する」用途にも使える、過去からある仕様です。

std::vector<int>&& iv1 = {1,3,3}; // 初期化子の寿命は切れるが、iv1が生存期間を延長
std::vector<int>&& iv2 = get_some_vector(); // 戻り値の生存期間を延長

しかし、Modern C++ではムーブセマンティクスやRVOなどがあるので、値束縛で十分だとも考えられます。

std::vector<int> iv3 = get_some_vector(); // RVO効いてるならこれで十分

右辺値参照は型のように見えるかも知れませんが、型と考えず、関数呼び出し時のパターンマッチと捉えるのが気楽だと思います。

実際、右辺値参照もすべて左辺値として扱われます。つまり、型の上では T&&T& と同じと扱われます。オーバーロード解決時にパターンマッチの要素として左辺値か右辺値かが問われるだけで、その参照が T&& として定義されたか、T& として定義されたかは問題にはなりません。

void foo(int&){}
void foo(int&&){}
void boo(int&& i){
    foo(i); // booの内部においてもfoo(int&)とマッチする。
}

int main()
{
    int&& i = 3; // iは右辺値参照として定義したが、i自体は左辺値
    foo(i); // iは左辺値なので、foo(int&)にマッチする。定義が int&& とされたこととは関係しない。
    boo(std::move(i)); // boo(int&&)とマッチするにはstd::moveが必要

    return 0;
}

テンプレートの読み書きをしない限り、関数引数、戻り値以外の右辺値参照は必要ないと言って良いでしょう。

std::move とは実際なんなのか

ここまでの立ち位置では std::move はただの魔法のキーワードで一体なんなのか、わからないままかも知れません。std::move がやっていることは、実際には TT&T&& にキャストしているだけです。

なぜそれで右辺値になるのか、ということなのですが、、、左辺値とか右辺値といった「値カテゴリ」というものが決まっています。C++17での値カテゴリの分類を右辺値/左辺値の区別の観点からざっくり説明すると下記です。

  • lvalue(左辺値)
    • 名前のあるものやその一部を表すもの(一部とは、例えばメンバ変数など)
    • 左辺値参照を返す関数(戻り値)/演算子の結果
    • 左辺値参照へのキャスト式
  • rvalue(右辺値)
    • prvalue(いわゆる生粋の右辺値)
      • 参照でない値を返す関数(戻り値)/演算子の結果
      • 参照でない型へのキャスト式
      • リテラル[1]
    • xvalue(強制されてできた右辺値)
      • 右辺値参照を返す関数(戻り値)/演算子の結果
      • 右辺値参照へのキャスト式

この分類をどう理解するか、ということですが、、、もともと左辺値、右辺値の概念があり、そこに参照をどう扱うかという考え方が追加されたというイメージです。追加されたような部分とは、lvalueの左辺値参照関係の仕様とxvalueの右辺値参照関係の仕様ですね。

std::move の話に戻ると、上記の分類から std::move はその引数を強制的に右辺値参照にキャストし、式全体としてはxvalueと認識されます。結果として右辺値と認識された結果、右辺値参照の引数にマッチするわけです。ややこしいですがそういうことです。

さて。ここから先に踏み込もうとすると、C++17とそれ以前の違いを強く意識することになります。C++17では左辺値と右辺値の定義が見直されただけでなく、初期化や戻り値の意味についても整理されています。この中ではRVOの位置づけなどもより明確になりますが、「ひとまずModern C++を読み書きできるようになる」目的からは大きく外れるように思われますので、本ガイドでは説明しないことにします。

脚注
  1. 文字列リテラルだけは特別に左辺値扱いです。これは理解の妨げになりかねませんが、こういう特別な仕様があるということは頭の片隅においておいて損はないかも知れません。 ↩︎