O'Reilly Early Releaseで購入したEffective Modern C++の読書記録です。
※この記事はあくまで私の備忘録です。(断りなく自分の解釈や考え・感想を入れたり、理由もなく内容を省略したり、間違いや曲解もあると思います。誤りの指摘は歓迎です。)
Item 28: reference collapsing (参照の縮約)
universal referenceでは引数にlvalueとrvalueのどちらが渡されたかの情報を持つ。
template<typename T> void func(T&& param);
universal referenceではlavalueが渡されるとTがlvalue referenceと型推定され、rvalueが渡されるとTはnon-reference (参照型では無い)とされる。
Widget widgetFactory(); // rvalueを返す関数 Widget w; func(w); // TはWidget&と型推定される func(widgetFactory()); // TはWidgetと型推定される
reference collapsing (参照の縮約)
C++では参照の参照は許されない。
int x; ... auto& & rx = x; // 参照の参照の宣言は不可 (コンパイルエラー)
template関数funcでは参照の参照が出現しているはず。
lvalue referenceを入れた場合に実体化される関数は以下のようになるからだ。
void func(Widget& && param);
しかしコンパイラはtemplateの実体化などの特別な文脈ではreference collapsing (参照の縮約) を行い、以下のように解釈される。
void func(Widget& param);
参照にはlvalue referenceとrvalue referenceの2種類あるから、参照の参照には4種類ある。
縮約のルール: どちらかのreferenceがlvalue referenceである場合はlvalue referenceになり、そうでなければrvalue referenceとなる。
上述の例の場合はlvalue reference Widget&へのrvalue referenceであるから、lvalue refereneceであるWidget&となる。
std::forward
std::forwardはこのreference collapsingが鍵となる。
template<typename T> void f(T&& fParam) { ... someFunc(std::forward<T>(fParam)); }
上記の典型的なstd::forwardの使用例において、fParamはuniversal referenceであるからTは引数としてlvalueとrvalueのどちらが渡されたのか(つまりfParamがどちらで初期化されたのか)を知っている。
したがってfにrvalueが渡された時、つまりTがnon-referenceと型推定される時にのみfParam (lvalue) がrvalueにキャストされる。
template<typename T> T&& forward(typename remove_reference<T>::type& param) { return static_cast<T&&>(param); }
上記のコードは(一部のインターフェースの詳細が省かれているため)標準規格準拠では無いが、std::forwardの振る舞いを示すコードとして十分である。
fにWidget型のlvalueが渡された場合
TはWidget&と型推定され、std::forwardはstd::forward<Widget&>として実体化される。
Widget& && forward(typename remove_reference<Widget&>::type& param) { return static_cast<Widget& &&>(param);}
std::remove_reference<Widget&>::typeはWidgetとなる。
Widget& && forward(Widget& param) { return static_cast<Widget& &&>(param);}
reference collapsingによってWidget& &&はWidget&に。
Widget& forward(Widget& param) { return static_cast<Widget&>(param);}
結果としてキャストは何もせず、lvalue referenceを返す。
fにWidget型のrvalueが渡された場合
TはWidgetと型推定され、std::forwardはstd::forward<Widget>として実体化される。
Widget&& forward(typename remove_reference<Widget>::type& param) { return static_cast<Widget&&>(param);}
std::remove_referenceをnon-referenceに適用してもWidgetのままであるから以下のようになる。
Widget&& forward(Widget& param) { return static_cast<Widget&&>(param);}
ここではreferenceに対するreferenceは無いためreference collapsingも起こらない。
結果としてstd::forwardはrvalueを返す。
つまり前段落のstd::forwardを呼び出すコードにおいて、fParamをrvalueにキャストしてsomeFuncへと渡すことになる。
ちなみにC++14にはstd::remove_reference_tという関数でより簡潔に記述できる。
template<typename T> T&& forward(remove_reference_t<T>& param) { return static_cast<T&&>(param);}
reference collapsingが起こる4つのコンテキスト
- templateの実体化
- auto変数
- typedefとalias declaration
- decltype
autoについてはtemplateの実体化と同様。
auto&& w1 = w; // => Widget& && w1 = w; Widget& w1 = w; // と解釈される
auto&& w2 = widgetFactory(); Widget&& w2 = widgetFactory(); // と解釈される
typedefについては以下の通り。
template<typename T> class Widget { public: typedef T&& RvalueRefToT; ... };
Widgetw; // => typedef int& && RvalueRefToT; // => typedef int& RvalueRefToT;
decltypeについてはitem 3を参照。
以上のことを踏まえると、universal referenceは何ら新しいreferenceではなく、実際には以下の条件を満たすrvalue referenceであることが分かる。
- lvalueとrvalueを区別して型推定が行われる、つまりTのlvalueはT&と型推定され、TのrvalueはTと型推定される
- reference collapsingが起こる