C++の基礎 - ユニバーサル参照
提供: MochiuWiki : SUSE, EC, PCB
📢 Webサイト閉鎖と移転のお知らせ
このWebサイトは2026年9月に閉鎖いたします。
新しい記事は移転先で追加しております。(旧サイトでは記事を追加しておりません)
概要
ユニバーサル参照 (universal reference) は、C++11で導入された特殊な参照型であり、転送参照 (forwarding reference) とも呼ばれる。
テンプレート引数の型推論と組み合わせることで、左辺値と右辺値の両方を受け取ることができる。
ユニバーサル参照は、C++の高度な機能であり、以下に示す事柄を理解する必要がある。
- 型推論が発生する場合の
T&&のみがユニバーサル参照である。 - 参照の折り畳み規則により、左辺値と右辺値を区別できる。
std::forwardと組み合わせて完全転送を実現する。- 名前付きの右辺値参照は左辺値として扱われる。
適切に使用することにより、柔軟なテンプレートコードを記述することができる。
ユニバーサル参照
ユニバーサル参照の概念
ユニバーサル参照は、T&&という記法で表現されるが、右辺値参照とは異なる動作をする。
以下に示す2つの条件を満たす場合にのみ、ユニバーサル参照となる。
- 型推論が発生する。(auto または テンプレートパラメータ)
T&&の形式である。(cvqualifier や 他の修飾なし)
ユニバーサル参照になる例
// 1. autoでの型推論
auto&& var = expression;
// 2. テンプレート関数の引数
template<typename T>
void func(T&& param);
// 3. テンプレートクラスのメンバ関数 (メンバ関数自体がテンプレート)
template<typename T>
class Widget
{
public:
template<typename U>
void process(U&& param); // ユニバーサル参照
};
ユニバーサル参照にならない例
// 右辺値参照 (ユニバーサル参照ではない) の例
// 1. 型が確定している
void func(std::string&& param); // 右辺値参照
// 2. constが付いている
template<typename T>
void func(const T&& param); // const右辺値参照
// 3. テンプレートクラスのメンバ関数 (クラスのテンプレートパラメータ)
template<typename T>
class Widget
{
public:
void process(T&& param); // 右辺値参照 (型推論が発生しない)
};
// 4. std::vectorの例
template<typename T>
class vector
{
public:
void push_back(T&& value); // 右辺値参照 (Tは既に確定している)
};
参照の折り畳み規則
折り畳みルール
ユニバーサル参照の動作を理解するには、参照の折り畳み規則を知る必要がある。
C++では、参照の参照は以下に示すルールで1つの参照に折り畳まれる。
右辺値参照同士の組み合わせのみが右辺値参照となり、それ以外は左辺値参照になる。
- T& & → T&
- T& && → T&
- T&& & → T&
- T&& && → T&&
型推論との組み合わせ
template<typename T>
void func(T&& param);
int x = 10;
// 左辺値を渡した場合
func(x); // Tはint&と推論される
// int& && → int& (参照の折り畳み)
// paramの型はint&
// 右辺値を渡した場合
func(10); // Tはintと推論される
// int&& がそのまま使われる
// paramの型はint&&
ユニバーサル参照の基本的な使用
autoとの組み合わせ
#include <iostream>
#include <string>
int main()
{
int x = 10;
const int cx = 20;
// 左辺値を束縛
auto&& uref1 = x; // int&と推論される
auto&& uref2 = cx; // const int&と推論される
// 右辺値を束縛
auto&& uref3 = 30; // int&&と推論される
auto&& uref4 = std::string("hello"); // std::string&&と推論される
// 型の確認 (コンパイル時エラーを利用)
// uref1の型を確認するには、以下をコメント解除
// decltype(uref1)* ptr = nullptr;
std::cout << "uref1: " << uref1 << std::endl; // 10
std::cout << "uref3: " << uref3 << std::endl; // 30
return 0;
}
テンプレート関数での使用
#include <iostream>
#include <string>
template<typename T>
void identify(T&& param)
{
std::cout << "Parameter received" << std::endl;
}
void testIdentify()
{
std::string str = "lvalue";
const std::string cstr = "const lvalue";
identify(str); // T = std::string&, param = std::string&
identify(cstr); // T = const std::string&, param = const std::string&
identify(std::string("rvalue"));// T = std::string, param = std::string&&
identify("temp"); // T = const char (&)[5], param = const char (&)[5]
}
詳細な型推論の例
その他のケース
#include <iostream>
#include <type_traits>
template<typename T>
void analyzeType(T&& param)
{
std::cout << "--- Type Analysis ---" << std::endl;
if (std::is_lvalue_reference<T>::value)
{
std::cout << "T is lvalue reference" << std::endl;
}
else if (std::is_rvalue_reference<T>::value)
{
std::cout << "T is rvalue reference" << std::endl;
}
else
{
std::cout << "T is not a reference" << std::endl;
}
if (std::is_lvalue_reference<decltype(param)>::value)
{
std::cout << "param is lvalue reference" << std::endl;
}
else if (std::is_rvalue_reference<decltype(param)>::value)
{
std::cout << "param is rvalue reference" << std::endl;
}
std::cout << std::endl;
}
int main()
{
int x = 10;
int& lref = x;
int&& rref = 20;
analyzeType(x); // T = int&, param = int&
analyzeType(lref); // T = int&, param = int&
analyzeType(rref); // T = int&, param = int& (rrefは左辺値)
analyzeType(10); // T = int, param = int&&
analyzeType(std::move(x)); // T = int, param = int&&
return 0;
}
const修飾の扱い
#include <iostream>
template<typename T>
void func(T&& param)
{
// 型情報の表示用
}
int main()
{
int x = 10;
const int cx = 20;
func(x); // T = int&, param = int&
func(cx); // T = const int&, param = const int&
func(10); // T = int, param = int&&
const int& clref = x;
func(clref); // T = const int&, param = const int&
return 0;
}
ユニバーサル参照の例
ファクトリ関数
#include <iostream>
#include <memory>
#include <string>
class Widget
{
private:
std::string name;
int value;
public:
Widget(std::string n, int v) : name(std::move(n)), value(v)
{
std::cout << "Widget constructed: " << name << std::endl;
}
void display() const
{
std::cout << "Widget: " << name << ", Value: " << value << std::endl;
}
};
// ユニバーサル参照を使用したファクトリ関数
template<typename T, typename... Args>
std::unique_ptr<T> make_unique_custom(Args&&... args)
{
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
int main()
{
std::string name = "Widget1";
// 左辺値を渡す
auto w1 = make_unique_custom<Widget>(name, 100);
// 右辺値を渡す
auto w2 = make_unique_custom<Widget>(std::string("Widget2"), 200);
// 混在
auto w3 = make_unique_custom<Widget>(name, 300);
w1->display();
w2->display();
w3->display();
return 0;
}
ラッパー関数
#include <iostream>
#include <chrono>
#include <string>
// 処理対象の関数
void process(std::string& str)
{
str += " (processed)";
std::cout << "Processing lvalue: " << str << std::endl;
}
void process(std::string&& str)
{
str += " (processed as rvalue)";
std::cout << "Processing rvalue: " << str << std::endl;
}
// ユニバーサル参照を使用したラッパー関数
template<typename T>
void logAndProcess(T&& param)
{
auto start = std::chrono::high_resolution_clock::now();
std::cout << "Logging before processing..." << std::endl;
process(std::forward<T>(param));
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::milli> elapsed = end - start;
std::cout << "Elapsed time: " << elapsed.count() << " ms" << std::endl;
}
int main()
{
std::string lvalue = "Left";
logAndProcess(lvalue); // 左辺値として転送
logAndProcess(std::string("Right")); // 右辺値として転送
logAndProcess(std::move(lvalue)); // 右辺値として転送
return 0;
}
範囲forループとの組み合わせ
#include <iostream>
#include <vector>
int main()
{
std::vector<int> numbers = {1, 2, 3, 4, 5};
// ユニバーサル参照を使用
// 要素が左辺値なので、auto&&はint&として推論される
for (auto&& num : numbers)
{
num *= 2; // 元のベクタの要素を変更
}
for (const auto& num : numbers)
{
std::cout << num << " "; // 2 4 6 8 10
}
std::cout << std::endl;
return 0;
}
完全転送との関係
ユニバーサル参照は、完全転送 (perfect forwarding) を実現するための基礎となる。
std::forward と組み合わせることにより、引数の値カテゴリを保持したまま別の関数に転送できる。
詳細は、C++の基礎 - 完全転送のページを参照すること。
その他 : ユニバーサル参照の注意
名前付きの右辺値参照は左辺値
この問題を解決するには、std::forwardを使用する必要がある。
#include <iostream>
void process(int& x)
{
std::cout << "Lvalue reference version" << std::endl;
}
void process(int&& x)
{
std::cout << "Rvalue reference version" << std::endl;
}
template<typename T>
void wrapper(T&& param)
{
// paramは名前を持つため、左辺値である
process(param); // 常に左辺値参照が呼ばれる
}
int main()
{
wrapper(10); // 右辺値を渡しても、左辺値参照が呼ばれる
int x = 20;
wrapper(x); // 左辺値参照が呼ばれる
return 0;
}
cv修飾との組み合わせはユニバーサル参照ではない
// ユニバーサル参照ではない例
template<typename T>
void func1(const T&& param); // const右辺値参照
template<typename T>
void func2(volatile T&& param); // volatile右辺値参照
template<typename T>
void func3(T* && param); // ポインタの右辺値参照
std::vectorとの違い
#include <vector>
template<typename T>
class MyVector
{
public:
// これはユニバーサル参照ではない
// Tは既にMyVectorのインスタンス化時に確定しているため
void push_back(T&& value);
};
// 使用例
MyVector<int> vec;
int x = 10;
// vec.push_back(x); // エラー : 左辺値を渡せない
vec.push_back(20); // OK: 右辺値
vec.push_back(std::move(x)); // OK: 右辺値にキャスト
型推論のデバッグ
コンパイル時の型確認
#include <iostream>
#include <typeinfo>
#include <type_traits>
template<typename T>
void printType(T&& param)
{
std::cout << "Type of T: " << typeid(T).name() << std::endl;
std::cout << "Type of param: " << typeid(param).name() << std::endl;
std::cout << "Is T lvalue reference: " << std::is_lvalue_reference<T>::value << std::endl;
std::cout << "Is T rvalue reference: " << std::is_rvalue_reference<T>::value << std::endl;
std::cout << "Is param lvalue reference: " << std::is_lvalue_reference<decltype(param)>::value << std::endl;
std::cout << "Is param rvalue reference: " << std::is_rvalue_reference<decltype(param)>::value << std::endl;
std::cout << "---" << std::endl;
}
int main()
{
int x = 10;
const int cx = 20;
printType(x); // T = int&
printType(cx); // T = const int&
printType(10); // T = int
printType(std::move(x)); // T = int
return 0;
}
意図的なコンパイルエラーを利用
template<typename T>
void func(T&& param)
{
// 型を確認するために意図的にエラーを起こす
// typename T::NonExistentType error;
}
int main()
{
int x = 10;
func(x); // コンパイルエラーのメッセージにTの型が表示される
return 0;
}
パフォーマンスへの影響
ユニバーサル参照を使用することにより、以下に示すメリットがある。
ただし、誤った使用は逆効果となる可能性があるため、std::forwardとの組み合わせが重要である。
- 不要なコピーを避けられる。
- 左辺値と右辺値の両方を効率的に処理できる。
- テンプレートコードの柔軟性が向上する。
- 完全転送により、パフォーマンスを損なわずに引数を転送できる。