参照型とvalue categoryについて

この記事のソースはN3690Value categories - cppreference.comc++ - What is "rvalue reference for *this"? - Stack Overflowです。

この記事の目標

非静的メンバ関数に適用される cv-qualification や ref-qualifier を、居心地よく使うための背景知識を整理したい。そのためには、オーバーロード解決について詳しく知る必要がありそう。

class X
{
    void f() const&; // ←とか
    void f() &&;     // ←を快適に使いたい
};

参照型

C++の式(Expression)はすべて、直交する概念として「型」と「value category」をもっている。
「型」については、intなどの基本型(3.9.1 Fundamental types)やポインタ・参照・クラスなどの複合型(3.9.2 Compound types)があり、この記事で興味があるのは、複合型のうち参照型である。参照型には左辺値参照(lvalue reference)と右辺値参照(rvalue reference)があるが(3.9.2p1)、これらはセマンティクス的に同等である(8.3.2p2)。
T&やT&&の変数宣言で初期化に使えるのは、T型のオブジェクトや関数、又はT型に変換可能なオブジェクトであり(8.5.3p1)、以下の場合に分けられる(8.5.3p5):

  • 参照型が左辺値参照で、初期化式がlvalue(ビットフィールドを除く)又は型変換させたlvalue
  • 参照型がconst型への左辺値参照(const T&)又は右辺値参照(T&&)で、
    • 初期化式がxvalue(ビットフィールドを除く)・クラスprvalue・配列prvalue・関数lvalue又は型変換させた…(左記の反復)
    • 初期化式がそれ以外の場合は、一時オブジェクトを作成。ただし、参照型が右辺値参照のとき、初期化式はlvalueであってはならない。一時オブジェクトのライフタイムは、基本的に参照のそれまで続く(12.2p9)。

cv修飾子

cv修飾子(cv-qualifier)には半順序が設定されており、以下の表で「より強いcv指定」を定義する(3.9.3p4)。

cv修飾子なし < const
cv修飾子なし < volatile
cv修飾子なし < const volatile
const        < const volatile
volatile     < const volatile

value category


すべての式(Expression)は、lvalue・xvalue・prvalueのどれかに分類される(3.10p1、value category)。
lvalue…オブジェクト又は関数を指し示す:

  • 普段使う変数や関数。型が右辺値参照であっても、変数それ自体のvalue categoryはlvalueであることに注意
  • 文字列リテラル(5.1.1p1)
  • 添字演算子[]の結果(5.2.1p1?)
  • 左辺値参照又は関数への右辺値参照を返す関数の呼出結果(5.2.2p10)
  • 逆参照演算子*の結果(5.3.1p1、3.10p1)
  • 前置++、--演算子の結果(5.3.2p1)
  • 組み込みの代入・複合代入演算子の結果(5.17p1)

xvalue(eXpiring value)…オブジェクト(たいていライフタイム終焉に近い)を表す:

  • オブジェクトへの右辺値参照を返す関数の呼出結果(5.2.2p10)。std::move(val)など

prvalue(pure rvalue)…一時オブジェクト、そのサブオブジェクト、オブジェクトに関連付けられていない値:

  • 文字列を除くリテラル(5.1.1p1、3.10p1)。12、7.3e5、true(2.14.6p1)、nullptr(2.14.7p1)
  • this(5.1.1p3-4、9.3.2p1)
  • ラムダ式(5.1.2p2)
  • 値で返す(参照で返さない)関数の呼出結果(5.2.2p10)
  • 後置++、--演算子の結果(5.2.6p1)
  • 単項演算子&、+、-、!、~の結果(5.3.1p2)
  • その他ほとんどの演算子の結果

状況により、結果のvalue categoryが変化する例:

  • C風キャスト式(5.4p1)、statict_cast(5.2.9p1)、reinterpret_cast(5.2.10p1)の結果は、左辺値参照又は関数への右辺値参照にキャストする場合lvalue、オブジェクトへの右辺値参照にキャストする場合xvalue、参照型にキャストしない場合prvalue
  • const_cast(5.2.11p1)の結果は、オブジェクトへの左辺値参照にキャストする場合lvalue、オブジェクトへの右辺値参照にキャストする場合xvalue、それ以外の場合prvalue
  • dynamic_cast(5.2.7p2)の結果は、ポインタ型にキャストする場合prvalue、左辺値参照にキャストする場合lvalue、右辺値参照にキャストする場合xvalue
  • クラスメンバアクセス.、->の結果は少し複雑(5.2.5p3-4)。xvalueなオブジェクトに対するメンバアクセスはxvalue
  • メンバへのポインタ.*、->*の結果は、左オペランドがlvalueで右オペランドがデータメンバへのポインタの場合lvalue、左オペランドがxvalueで右オペランドがデータメンバへのポインタの場合xvalue、右オペランドメンバ関数へのポインタの場合(5.5p6)

これらの総称として、以下のカテゴリがある:

  • glvalue(generalized lvalue)…lvalue又はxvalue
  • rvalue…xvalue又はprvalue

prvalueが期待されるコンテキストでglvalueを使うと、変換されてprvalueになる。例外として、右辺値参照はlvalueで初期化できない(3.10p2)。非関数・非配列のglvalueは、prvalueに(4.1 Lvalue-to-rvalue conversion)、Tの配列はTへのポインタを表すprvalueに(4.2 Array-to-pointer conversion)、関数型TのlvalueはTへの関数ポインタを表すprvalueに(4.3 Function-to-pointer conversion)それぞれ標準変換される。

Rvalue references for *this

C++11以降では、メンバ関数に「cv指定」だけでなく「ref指定(ref-qualifier)」を書くことができる:

class X
{
    void f() &;  // この& がref-qualifier
    void f() &&; // この&&がref-qualifier
};

このようにすることで、オブジェクトのvalue categoryに応じて、異なる関数を呼び出せる:

X x;
x.f();   // x  はlvalue。f() & を呼ぶ
X().f(); // X()はrvalue。f() &&を呼ぶ

一番上にあるコード例のように、従来からお馴染みの「cv指定」も混ぜて指定できる。関数宣言で「cv指定」や「ref指定」を書く位置は次の通り:

T f( parameter-declaration-clause ) cv-qualifier-seq ref-qualifier exception-specification attribute-specifier-seq

つまり、返却値型 関数名(仮引数リスト) cv指定 ref指定 例外指定 属性指定、の順(8.3.5p1)。cv-qualifier-seqやref-qualifierは関数型の一部(8.3.5p6)であり、異なる「cv指定」や「ref指定」で関数をオーバーロードできる。

オーバーロード解決

ref-qualifierでオーバーロードさせる場合、候補関数はすべてref-qualifierを付けなければならない(13.1p2)。つまり、候補関数にはすべてref-qualifierが付いているか、すべてref-qualifierを欠いているか、どちらかでなければならず、一部の候補関数だけref-qualifierを付けることはできない。以下は規格書からの引用:

class Y {
    void h() &;
    void h() const &; // OK
    void h() &&;      // OK, all declarations have a ref-qualifier
    void i() &;
    void i() const;   // ill-formed, prior declaration of i
                      // has a ref-qualifier
};

メンバ関数は、呼ばれているオブジェクト(*this)を表す追加の仮引数(implicit object parameter)をもっており、呼び出し側ではこれと対応するように、操作対象オブジェクトを表す実引数(implied object argument)を追加し、これらはどちらも第一引数になる(13.3.1p2-3)。非静的メンバ関数では、implicit object parameterの型は次のようになる(13.3.1p4):

関数に「ref-qualifierがない」又は「ref-qualifierとして&が指定された」場合…cv X&
関数に「ref-qualifierとして&&が指定された」場合…cv X&&
(但し、Xはメンバ関数をもつクラス、cvはメンバ関数のcv指定)

関数に「ref-qualifierがない」場合は、追加のルールが適用される:

implicit object parameterがconst修飾されていなくても(つまり仮引数の型はX&)、
対応する実引数から型変換できるのであれば、rvalueであっても仮引数にバインドできる(13.3.1p4)。

implicit object parameterだけが例外であり、普段は仮引数に参照型が含まれる場合、参照をバインドする(実引数での初期化を試す)工程があって、ここでは非constな左辺値参照はrvalueにバインドできないことや、右辺値参照は(関数以外の)lvalueにバインドできないことが、考慮される(13.3.2p3、13.3.3.1.4p3)。オーバーロード解決では、実引数の型・cv指定・value categoryと、これらが対応する仮引数にマッチするためにどれくらい変換されるかだけが、考慮に入れられる(13.3.3.1p2)。

具体的な指針(13.3.3.2p3)

「ref-qualifierがない」非静的メンバ関数のimplicit object parameterを考えない場合(この条件どうなんだろ?)、rvalueが左辺値参照よりも右辺値参照にバインドする方が有利。この条件から、よくあるf(const X&)〔又はf(X&)〕とf(X&&)のオーバーロード解決の方針が出てくる。以下は規格書からの引用:

int i;
int f1();
int&& f2();
int g(const int&);
int g(const int&&);

int j = g(i);             // calls g(const int&)
int k = g(f1());          // calls g(const int&&)
int l = g(f2());          // calls g(const int&&)

struct A {
    A& operator<<(int);
    void p() &;
    void p() &&;
};
A& operator<<(A&&, char);

A() << 1;                 // calls A::operator<<(int)
A() << ’c’;             // calls operator<<(A&&, char)
A a;
a << 1;                   // calls A::operator<<(int)
a << ’c’;               // calls A::operator<<(int)
A().p();                  // calls A::p()&&
a.p();                    // calls A::p()&

(以下の条件は、この記事とは直接関係ないです。おまけ)
そうでない場合、関数lvalueが右辺値参照よりも左辺値参照にバインドする方が有利。以下は規格書からの引用:

int f(void(&)());  // #1
int f(void(&&)()); // #2
void g();
int i1 = f(g);     // calls #1

そうでない場合、cv修飾の変換が少ない方が有利。以下は規格書からの引用:

int f(const int *);
int f(int *);
int i;
int j = f(&i);      // calls f(int*)

そうでない場合、参照のバインドでより強いcv修飾子を仮引数にもつ方が不利。以下は規格書からの引用:

int f(const int &);
int f(int &);
int g(const int &);
int g(int);

int i;
int j = f(i);             // calls f(int &)
int k = g(i);             // ambiguous

struct X {
    void f() const;
    void f();
};
void g(const X& a, X b) {
    a.f();                // calls X::f() const
    b.f();                // calls X::f()
}