C++の基礎 - ユニバーサル参照

2026年1月6日 (火) 01:36時点におけるWiki (トーク | 投稿記録)による版 (ページの作成:「== 概要 == ユニバーサル参照 (universal reference) は、C++11で導入された特殊な参照型であり、<u>転送参照</u> (forwarding reference) とも呼ばれる。<br> テンプレート引数の型推論と組み合わせることで、左辺値と右辺値の両方を受け取ることができる。<br> <br> ユニバーサル参照は、C++の高度な機能であり、以下に示す事柄を理解する必要がある。<br> * 型推論が…」)
(差分) ← 古い版 | 最新版 (差分) | 新しい版 → (差分)
📢 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との組み合わせが重要である。

  • 不要なコピーを避けられる。
  • 左辺値と右辺値の両方を効率的に処理できる。
  • テンプレートコードの柔軟性が向上する。
  • 完全転送により、パフォーマンスを損なわずに引数を転送できる。