2017年1月17日火曜日

【読書ノート】Effective Modern C++ - Item 28

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;
  ...
};
Widget w;
// => typedef int& && RvalueRefToT;
// => typedef int& RvalueRefToT;

decltypeについてはitem 3を参照。

以上のことを踏まえると、universal referenceは何ら新しいreferenceではなく、実際には以下の条件を満たすrvalue referenceであることが分かる。

  • lvalueとrvalueを区別して型推定が行われる、つまりTのlvalueはT&と型推定され、TのrvalueはTと型推定される
  • reference collapsingが起こる