右辺値参照宣言子: &&
右辺値の式への参照を保持します。
type-id && cast-expression
解説
右辺値参照を使用すると、左辺値を右辺値と区別できます。左辺値参照と右辺値参照は構文的および意味的に似ていますが、従う規則が少し異なります。lvalues および rvalues の詳細については、「左辺値と右辺値」を参照してください。lvalue 参照の詳細については、「左辺値参照宣言子: と」を参照してください。
ここからのセクションでは、右辺値参照がどのように移動セマンティクスと完全転送の実装をサポートするかについて説明します。
移動セマンティクス
右辺値参照は、アプリケーションのパフォーマンスを大幅に向上させることができる移動セマンティクスの実装をサポートします。移動セマンティクスにより、オブジェクト間でリソースを転送するコード (動的に割り当てられるメモリなど) を記述できます。移動セマンティクスにより、プログラムで参照できない一時オブジェクトからリソースを転送できます。
移動セマンティクスを実装する場合、通常はクラスに対して移動コンストラクターを指定し、オプションで移動代入演算子 (operator=) を指定します。コピー、およびソースが右辺値である代入演算では、その後で移動セマンティクスが自動的に利用されます。既定のコピー コンストラクターとは異なり、このコンパイラには既定の移動コンストラクターがありません。移動コンストラクターの作成方法およびアプリケーションでの移動コンストラクターの使用方法の詳細については、「方法: 移動コンストラクターを作成します」を参照してください。
移動セマンティクスを活用するために、通常の関数と演算子をオーバーロードすることもできます。Visual C++ 2010 によって、標準テンプレート ライブラリ (STL) への移動セマンティクスが導入されます。たとえば、string クラスは、移動セマンティクスを実行する操作を実装します。複数の文字列を連結し、結果を出力する次の例を考えます。
// string_concatenation.cpp
// compile with: /EHsc
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s = string("h") + "e" + "ll" + "o";
cout << s << endl;
}
Visual C++ 2010 の前に、operator+ に対する各呼び出しは、新しい一時 string オブジェクト (右辺値) を割り当てて返します。operator+ は、ソース文字列が左辺値であるか右辺値であるかわからないため、文字列を他の文字列に追加できません。ソース文字列が両方とも左辺値である場合、それらのソース文字列はプログラム内の他の場所で参照されている場合があるため、変更しないでください。右辺値の参照を使用して、プログラムの別の場所で参照できない右辺値を受け入れるように、operator+ を変更できます。したがって、operator+ は 1 つの文字列を別の文字列に追加できるようになりました。これによって、string クラスが実行する必要がある動的メモリ割り当ての数を大幅に減らすことができます。string クラスの詳細については、「basic_string Class」を参照してください。
また、コンパイラが戻り値の最適化 (RVO) または名前付き戻り値の最適化 (NRVO) を使用できない場合に、移動セマンティクスが役立ちます。このような場合、型が移動コンストラクターを定義していれば、コンパイラはその移動コンストラクターを呼び出します。名前付き戻り値の最適化の詳細については、「Named Return Value Optimization in Visual C++ 2005 (Visual C++ 2005 での名前付き戻り値の最適化)」を参照してください。
移動セマンティクスをより理解するために、vector オブジェクトに要素を挿入する例について考えてみましょう。vector オブジェクトの容量が超過した場合、vector オブジェクトは、その要素にメモリを再割り当てし、別のメモリ位置に各要素を挿入して、挿入する要素のための領域を作成する必要があります。挿入操作が要素をコピーするときは、新しい要素を作成し、コピー コンストラクターを呼び出して前の要素から新しい要素にデータをコピーした後、前の要素を破棄します。移動セマンティクスにより、メモリ割り当てとコピー操作を実行する必要なく、オブジェクトを直接移動できます。
vector の例に含まれる移動セマンティクスを使用するには、オブジェクト間でデータを移動する移動コンストラクターを記述します。
Visual C++ 2010 での STL へのセマンティクスの移動の概要については、「C++ の標準ライブラリの参照」を参照してください。
完全転送
完全転送により、オーバーロード関数の必要性が低くなり、転送の問題を回避できます。転送の問題は、ユーザーがパラメーターとして参照を取るジェネリック関数を書き、この関数がこれらのパラメーターを他の関数に渡す (または転送する) ときに発生する可能性があります。たとえば、ジェネリック関数が const T& 型のパラメーターを受け取る場合、呼び出される関数はそのパラメーターの値を変更できません。T& 型のジェネリック関数がパラメーターを受け取る場合、関数は rvalue を使用して呼び出すことはできません (一時的なオブジェクトや整数リテラルなど)。
通常、この問題を解決するには、パラメーターごとに T& と const T& の両方を受け取るジェネリック関数のオーバーロードされたバージョンを提供する必要があります。その結果、オーバーロードされた関数の数は、パラメーターの数と共に指数関数的に増加します。右辺値の参照を使用すると、任意の引数を受け入れて、他の関数が直接呼び出されたかのように別の関数に転送する、任意のバージョンの関数を作成できます。
W、X、Y、および Z の 4 つの型を宣言する次の例を考えます。各型のコンストラクターは、const および非 const lvalue 参照の異なる組み合わせをパラメーターとして受け取ります。
struct W
{
W(int&, int&) {}
};
struct X
{
X(const int&, int&) {}
};
struct Y
{
Y(int&, const int&) {}
};
struct Z
{
Z(const int&, const int&) {}
};
オブジェクトを生成するジェネリック関数を記述するとします。この関数を作成する方法の 1 つを次の例に示します。
template <typename T, typename A1, typename A2>
T* factory(A1& a1, A2& a2)
{
return new T(a1, a2);
}
次の例は、factory 関数への有効な呼び出しを示します。
int a = 4, b = 5;
W* pw = factory<W>(a, b);
ただし、factory はそのパラメーターとして変更可能な左辺値の参照を受け取りますが、右辺値を使用して呼び出されるため、次の例には factory 関数の有効な呼び出しは含まれていません。
Z* pz = factory<Z>(2, 2);
通常、この問題を解決するには、A& および const A& パラメーターの任意の組み合わせの factory 関数のオーバーロードされたバージョンを作成する必要があります。右辺値参照を使用すると、次の例に示すように、任意のバージョンの factory 関数を作成できます。
template <typename T, typename A1, typename A2>
T* factory(A1&& a1, A2&& a2)
{
return new T(std::forward<A1>(a1), std::forward<A2>(a2));
}
この例では、factory 関数へのパラメーターとして、右辺値参照を使用しています。std::forward 関数の目的は、ファクトリ関数のパラメーターをテンプレート クラスのコンストラクターに転送することです。
次の例は、W、X、Y、Z クラスのインスタンスを作成するために、変更済みの factory 関数を使用する main 関数を示します。変更された factory 関数は、そのパラメーター (左辺値または右辺値) を適切なクラス コンストラクターに転送します。
int main()
{
int a = 4, b = 5;
W* pw = factory<W>(a, b);
X* px = factory<X>(2, b);
Y* py = factory<Y>(a, 2);
Z* pz = factory<Z>(2, 2);
delete pw;
delete px;
delete py;
delete pz;
}
Rvalue 参照の追加プロパティ
関数をオーバーロードして、lvalue 参照と rvalue 参照を受け取ることができます。
関数をオーバーロードして const の左辺値の参照または右辺値の参照を取得することによって、変更できないオブジェクト (左辺値) と変更可能で一時的な値 (右辺値) を区別するコードを記述できます。オブジェクトが const としてマークされていない限り、rvalue 参照を受け取る関数にオブジェクトを渡すことができます。次の例は、lvalue 参照と rvalue 参照を取得するためにオーバーロードされる関数 f を示しています。main 関数は、左辺値と右辺値の両方を持つ f を呼び出します。
// reference-overload.cpp
// Compile with: /EHsc
#include <iostream>
using namespace std;
// A class that contains a memory resource.
class MemoryBlock
{
// TODO: Add resources for the class here.
};
void f(const MemoryBlock&)
{
cout << "In f(const MemoryBlock&). This version cannot modify the parameter." << endl;
}
void f(MemoryBlock&&)
{
cout << "In f(MemoryBlock&&). This version can modify the parameter." << endl;
}
int main()
{
MemoryBlock block;
f(block);
f(MemoryBlock());
}
この例を実行すると、次の出力が生成されます。
In f(const MemoryBlock&). This version cannot modify the parameter.
In f(MemoryBlock&&). This version can modify the parameter.
この例では、f への最初の呼び出しが、引数としてローカル変数 (左辺値) を渡します。f への 2 番目の呼び出しは引数として一時オブジェクトを渡します。一時オブジェクトはプログラムの他の場所で参照できないため、呼び出しはオブジェクトを自由に変更できる右辺値の参照を受け取る f のオーバーロードされたバージョンにバインドされます。
コンパイラは、名前付きの rvalue 参照を lvalue、名前のない rvalue 参照を rvalue として処理します。
rvalue 参照をパラメーターとして受け取る関数を記述すると、そのパラメーターが関数本体の lvalue として扱われます。名前付きオブジェクトはプログラムの複数の部分から参照できるため、コンパイラは名前付きの rvalue を lvalue として処理します。プログラムの複数の部分からのオブジェクト リソースの変更または削除を許可することは危険です。たとえば、プログラム内の複数の部分で同じオブジェクトからリソースを転送しようとすると、リソースが正常に転送されるのは最初の部分だけです。
次の例は、lvalue 参照と rvalue 参照を取得するためにオーバーロードされる関数 g を示しています。関数 f は右辺値参照をパラメーター (名前付きの右辺値参照) として取り、右辺値参照 (名前のない右辺値参照) を返します。f から g への呼び出しでは、f の本体が左辺値としてパラメーターを処理するので、オーバーロードの解決は、左辺値参照を取得する g のバージョンを選択します。main から g への呼び出しでは、f が右辺値参照を返すので、オーバーロードの解決は、右辺値参照を取得する g のバージョンを選択します。
// named-reference.cpp
// Compile with: /EHsc
#include <iostream>
using namespace std;
// A class that contains a memory resource.
class MemoryBlock
{
// TODO: Add resources for the class here.
};
void g(const MemoryBlock&)
{
cout << "In g(const MemoryBlock&)." << endl;
}
void g(MemoryBlock&&)
{
cout << "In g(MemoryBlock&&)." << endl;
}
MemoryBlock&& f(MemoryBlock&& block)
{
g(block);
return block;
}
int main()
{
g(f(MemoryBlock()));
}
この例を実行すると、次の出力が生成されます。
In g(const MemoryBlock&).
In g(MemoryBlock&&).
この例では、main 関数は、右辺値を f に渡します。f の本体は、名前付きパラメーターを左辺値として処理します。f から g への呼び出しにより、パラメーターは lvalue 参照 (g の最初にオーバーロードされたバージョン) にバインドされます。
- rvalue 参照に lvalue をキャストできます。
STL std::move 関数を使用すると、オブジェクトを、そのオブジェクトへの rvalue 参照に変換することができます。または、次の例に示すように、static_cast キーワードを使用して、左辺値を右辺値の参照にキャストできます。
// cast-reference.cpp
// Compile with: /EHsc
#include <iostream>
using namespace std;
// A class that contains a memory resource.
class MemoryBlock
{
// TODO: Add resources for the class here.
};
void g(const MemoryBlock&)
{
cout << "In g(const MemoryBlock&)." << endl;
}
void g(MemoryBlock&&)
{
cout << "In g(MemoryBlock&&)." << endl;
}
int main()
{
MemoryBlock block;
g(block);
g(static_cast<MemoryBlock&&>(block));
}
この例を実行すると、次の出力が生成されます。
In g(const MemoryBlock&).
In g(MemoryBlock&&).
関数テンプレートは、テンプレート引数の型を推測してから、参照縮小規則を使用します。
通常は、別の関数にパラメーターを渡す (または転送する) 関数テンプレートを作成します。テンプレート型推論が、右辺値の参照を受け取る関数テンプレートに対してどのように機能するか理解しておくことは重要です。
関数の引数が右辺値の場合、引数は rvalue 参照であると推測されます。たとえば、パラメーターとして T&& 型を受け取るテンプレート関数に、X 型のオブジェクトへの rvalue 参照を渡すと、テンプレート引数の推論は X になるように T を推論します。したがって、パラメーターの型は X&& になります。関数の引数が左辺値または const 左辺値の場合、その型は lvalue 参照またはその型の const lvalue 参照であると推測されます。
次の例では、1 つの構造テンプレートを宣言し、それをさまざまな参照型に特化します。print_type_and_value 関数は、パラメーターとして右辺値参照を取り、S::print メソッドの適切な特殊化バージョンそれを転送します。main 関数は S::print メソッドを呼び出すさまざまな方法を示します。
// template-type-deduction.cpp
// Compile with: /EHsc
#include <iostream>
#include <string>
using namespace std;
template<typename T> struct S;
// The following structures specialize S by
// lvalue reference (T&), const lvalue reference (const T&),
// rvalue reference (T&&), and const rvalue reference (const T&&).
// Each structure provides a print method that prints the type of
// the structure and its parameter.
template<typename T> struct S<T&> {
static void print(T& t)
{
cout << "print<T&>: " << t << endl;
}
};
template<typename T> struct S<const T&> {
static void print(const T& t)
{
cout << "print<const T&>: " << t << endl;
}
};
template<typename T> struct S<T&&> {
static void print(T&& t)
{
cout << "print<T&&>: " << t << endl;
}
};
template<typename T> struct S<const T&&> {
static void print(const T&& t)
{
cout << "print<const T&&>: " << t << endl;
}
};
// This function forwards its parameter to a specialized
// version of the S type.
template <typename T> void print_type_and_value(T&& t)
{
S<T&&>::print(std::forward<T>(t));
}
// This function returns the constant string "fourth".
const string fourth() { return string("fourth"); }
int main()
{
// The following call resolves to:
// print_type_and_value<string&>(string& && t)
// Which collapses to:
// print_type_and_value<string&>(string& t)
string s1("first");
print_type_and_value(s1);
// The following call resolves to:
// print_type_and_value<const string&>(const string& && t)
// Which collapses to:
// print_type_and_value<const string&>(const string& t)
const string s2("second");
print_type_and_value(s2);
// The following call resolves to:
// print_type_and_value<string&&>(string&& t)
print_type_and_value(string("third"));
// The following call resolves to:
// print_type_and_value<const string&&>(const string&& t)
print_type_and_value(fourth());
}
この例を実行すると、次の出力が生成されます。
print<T&>: first
print<const T&>: second
print<T&&>: third
print<const T&&>: fourth
print_type_and_value 関数の各呼び出しを解決するため、コンパイラは最初にテンプレート引数の推論を実行します。次に、コンパイラは、引数パラメーター型を推測されたテンプレートに置き換えるときに参照縮小規則を適用します。たとえば、print_type_and_value 関数にローカル変数 s1 を渡すと、コンパイラで次の関数シグネチャが生成されます。
print_type_and_value<string&>(string& && t)
コンパイラは、参照縮小規則を使用して以下のシグネチャを減らします。
print_type_and_value<string&>(string& t)
このバージョンの print_type_and_value 関数は、S::print メソッドの正しい特化されたバージョンにパラメーターを転送します。
次の表は、テンプレートの引数の型を推論するときの参照縮小規則をまとめたものです。
展開された型 |
折りたたまれた型 |
T& & |
T& |
T& && |
T& |
T&& & |
T& |
T&& && |
T&& |
テンプレート引数の推論は、完全転送を実装するうえでの重要な要素です。「完全転送」セクションでは、このトピックで先ほど触れた完全転送について詳細に説明します。
概要
右辺値参照は、左辺値を右辺値と区別します。これらは不要なメモリ割り当てとコピー操作の必要性をなくすことでアプリケーションのパフォーマンスを向上させることができます。これにより、任意の引数を受け入れて、他の関数が直接呼び出されたかのように別の関数に転送する、任意のバージョンの関数を作成することもできます。