値カテゴリと参照型には明確な違いがあります。値カテゴリは式が指す対象の性質を分類するものであり、左辺値はメモリ上に存在し識別子を持つ値、右辺値は一時的なオブジェクトや計算結果です。一方、参照型は変数の型として宣言されるもので、&や&&などの記号で表現されます。
以下は実際のコード例です:
#include <iostream>
using namespace std;
void process_value(int& ref) {
cout << "左辺値参照" << endl;
}
void process_value(int&& temp_ref) {
cout << "右辺値参照" << endl;
}
int main()
{
int num = 42;
int& lvalue_ref = num;
int&& rvalue_ref = move(num);
process_value(num);
process_value(lvalue_ref);
process_value(rvalue_ref);
process_value(forward<int>(rvalue_ref));
process_value(100);
return 0;
}
Visual Studio 2022での出力結果は以下の通りです。変数numは左辺値として型推論されますが、rvalue_refが左辺値参照として出力される理由は、右辺値参照型の変数自体は左辺値であるという重要な概念を理解する必要があります。
rvalue_refは型がint&&の右辺値参照ですが、変数として存在するため左辺値として扱われます。このため関数呼び出しでは左辺値参照のオーバーロードが選択されます。
万能参照と参照折り畳み
万能参照とは、テンプレートパラメータのT&&またはauto&&の形式で、型推論が行われる場合にのみ有効になります。重要なのは、すべてのT&&が万能参照になるわけではないことです。
例えば以下のコードを見てください:
template<typename ElementType, typename Alloc = allocator<ElementType>>
class container
{
public:
void insert_item(ElementType&& item);
// ...
};
insert_itemメソッドの引数はT&&ですが、containerクラスを使用する際にElementTypeが明示的に指定されるため、型推論は発生しません。したがって、container<SomeType>として使用した場合、引数はSomeType&&として扱われます。
テンプレートにおける型推論の違い
型Tの左辺値に対してはテンプレートはT&として推論されますが、型Tの右辺値に対してはTとして推論されます。これは非常に重要な違いです。
template<typename DataType>
void display_type(DataType& data) {
cout << "左辺値受取" << endl;
}
template<typename DataType>
void display_type(DataType&& data) {
cout << "右辺値受取" << endl;
}
template<typename DataType>
void handle_data(DataType&& input) {
display_type(input);
display_type(forward<DataType>(input));
}
int main()
{
int sample = 5;
int& left_ref = sample;
int&& right_ref = move(sample);
cout << "sample:" << endl;
handle_data(sample);
cout << "left_ref:" << endl;
handle_data(left_ref);
cout << "right_ref:" << endl;
handle_data(right_ref);
cout << "literal:" << endl;
handle_data(7);
return 0;
}
最初の3つのケース(sample、left_ref、right_ref)はすべて左辺値であるため、型推論の結果としてT=int&となり、テンプレートパラメータのT&&はint&&になります。しかし参照折り畳みのルールにより、int&&&はint&に簡約されるため、関数パラメータは左辺値参照として扱われます。
リテラル値7の場合、これは右辺値なのでT=intとして推論され、T&&はint&&となり、右辺値参照として正しくマッチします。
std::forwardの動作原理
関数内部で受け取った値は、その型に関わらず左辺値として扱われるため、forward関数を使用して元の値カテゴリを維持する必要があります。forwardは型情報を保持しつつ、適切な値カテゴリで転送を行います。
std::moveの実装
std::moveの基本的な実装は以下のようになります:
template<typename SourceType>
typename remove_reference_t<SourceType>&&
move(SourceType&& source)
{
using TargetType = typename remove_reference_t<SourceType>&&;
return static_cast<TargetType>(source);
}
この実装からわかるように、戻り値は右辺値参照型となっています。関数の戻り値型によって返される値のカテゴリが決まります:
値渡しの場合は右辺値 左辺値参照の場合は左辺値 右辺値参照の場合は右辺値
Effective Modern C++の原則
Item 25の重要な原則として、「右辺値参照にはstd::move、万能参照にはstd::forwardを使用する」というものがあります。この原則に従うことで、移動セマンティクスを正しく活用できます。
std::moveとstd::forwardは元の引数を変更せず、単に型情報を保持して転送するだけです。実際に変更が発生するのは、ムーブコンストラクタやコピーコンストラクタなどの処理においてです。