【C++】ユニバーサル参照、完全転送【T&& auto&& の意味や目的】

ユニバーサル参照や完全転送の意味や役割、目的を解説します。普段何気なく見かけているT&&auto&&の理解に繋がる内容となっています。

目次

ユニバーサル参照

ユニバーサル参照(universal reference)とは、左辺値参照と右辺値参照のいずれかの性質を持つことのできる特殊な参照のことです。ユニバーサル参照として宣言された変数の型や仮引数の型は、変数や実引数に与えられた値の種類によって、左辺値参照型と右辺値参照型のいずれかに変化します。

ユニバーサル参照はauto型の変数宣言時やテンプレートの仮引数の宣言時にauto&&T&&という形式で宣言する事ができます。

int i = 9;
auto&& a = i; // int&  a;と等価 (左辺値参照型)
auto&& b = 9; // int&& b;と等価 (右辺値参照型)

上記の例では、変数aは左辺値参照を受け取るint&型の宣言として解釈されます。逆に変数bの場合は右辺値参照を受け取るint&&型の宣言として解釈されます。

テンプレート関数の場合も同様に、実引数として渡された値の種類によって、テンプレート仮引数側の型の種類が変化します。

template<class T> void f(T&& v) {}

int i = 9;
f(i); // 仮引数vは int&
f(9); // 仮引数vは int&&

ちなみに、ユニバーサル参照は「ユニヴァーサル参照」と呼ばれることもあります。

完全転送

完全転送(Perfect Forwarding)とは、左辺値参照や右辺値参照などの型に応じた適切な関数呼び出しを実現するための手法やその概念のことです。完全転送によって、左辺値参照や右辺値参照などの型情報とその値を他の異なる関数へと引き継ぐことが可能となります。

template<class U> void g(U& v)  {}
template<class U> void g(U&& v) {}
template<class T> void f(T&& v) { g(std::forward<T>(v)/* 完全転送 */); }

int i = 9;
f(i); // `g(U& v)` が呼ばれる
f(9); // `g(U&& v)`が呼ばれる

このようにf関数のユニバーサル参照に渡された仮引数が左辺値参照(T&)相当の場合には、左辺値参照を受け取る関数g(U& v)を呼び出し、逆に右辺値参照(T&&)の場合には、右辺値参照を受け取る関数g(U&& v)を呼び出すような挙動となります。

std::forward<T>()による完全転送を行わなかった場合、右辺値参照として受け取った仮引数の値は左辺値参照として他の関数に渡されてしまいます。

template<class U> void g(U& v)  {}
template<class U> void g(U&& v) {}
template<class T> void f(T&& v) { g(v/* std::forward<T>(v) */); }

f(9); // `g(U& v)`が呼ばれてしまう

上記の例では型情報ではなくg(v)という文脈によって実引数の推定が行われてしまうため、左辺値参照を受け取るg(U& v)関数が呼び出される事になります。

そのため、このような問題を回避するために、先程のstd::forward<T>()による完全転送が必要となるのです。なお、右辺値参照を受け取る関数を明示的に呼び出すために、std::movestatic_cast<T&&>()による右辺値参照への明示的なキャストを用いることもできます。

template<class U> void g(U& v)  {}
template<class U> void g(U&& v) {}

int&& v = 9;
g(std::move(v));          // `g(U&& v)`
g(static_cast<int&&>(v)); // `g(U&& v)`
g(v);                     // `g(U& v)`

ただ、実際にユニバーサル参照(T&&)の値を他の関数に転送する場合には、std::moveではなくstd::forward<T>()による完全転送を用いる必要があります。ユニバーサル参照に左辺値参照(T&)の値が渡ってきた場合、std::moveはそれを右辺値参照(T&&)に変換してしまうためです。値を右辺値参照として転送した先で、ムーブコンストラクタを介した変換やムーブ代入が働いた場合、元の左辺値参照の値は不定な値に変化します。

std::vector<std::string> a;
template<class U> void g(U& v)  { a.push_back(v); /* 単純に値コピーが発生 */ }
template<class U> void g(U&& v) { a.push_back(std::move(v)); /* 所有権が移動する */ }
template<class T> void f(T&& v) { g(std::move(v)/* 危険 */); }

std::string s = "string";
std::cout << s; // 出力結果: "string"
f(s);           // `g(U&& v)`に転送されてしまう
std::cout << s; // 出力結果: "" (不定 - ムーブセマンティクスの影響)
参考: ムーブコンストラクタとムーブセマンティクス

逆にstd::forward<T>(v)を用いれば、vが右辺値参照型の場合には右辺値参照型としてキャストが行われ(static_cast<T&&>)、左辺値参照型の場合には右辺値参照型としてキャストが行われるようになります(static_cast<T&>)。

このように完全転送は、左辺値を左辺値、右辺値を右辺値として適切に処理するための重要な仕組みとなっています。

for文におけるauto&&の意味と目的

コンテナの要素を走査する際には、慣用的にfor (auto&& 要素 : コンテナ)という記法を用いることがあります。この、要素をユニバーサル参照で受け取る作法は、イテレータが左辺値を返す際の問題に対処する目的で利用されています。

実際、std::vector<bool>のイテレータは要素への参照ではなく、要素を参照するヘルパークラスを返すため、拡張for文側では要素を左辺値参照として受け取ることができないという問題に遭遇します。

std::vector<bool> a = {0, 0, 0};
for (auto& r : a) { r = 1; }
//         ^ ~
// error: non-const lvalue reference to type '__bit_reference<[2 * ...]>' cannot bind to a temporary of type '__bit_reference<[2 * ...]>'

変数rにはヘルパークラス(std::__bit_reference)によって表現された一時オブジェクト(右辺値)が渡ってくるため、正しくはauto rauto&& rといった宣言で値を受け取る必要があるのです。

std::vector<bool> a = {0, 0, 0};
for (auto r : a)   { r = 1; }   // ok
for (auto&& r : a) { r = 1; }   // ok

/* ちなみに`v = 1`による要素への操作はヘルパークラス側が代行してくれている(よって見た目上は左辺値参照への操作と変わらない) */
// a == {1, 1, 1}

/* const左辺値参照として値を受け取ることも可能だが、値の変更は行えない */
for (const auto& r : a) { r = 1; } // error: no viable overloaded '='
for (const auto& v : a) { std::cout << v; } // ok: 参照のみ

そのため、ジェネリックプログラミング等の汎用的な分野では、拡張for文を利用する際にはauto&&による記述を用いるのが慣習となっています。auto&&によるユニバーサル参照を活用すれば、左辺値も右辺値も一律に受け取ることができるため、汎用的な処理を実現するための重要な作法となっています。

広告