完全転送 解答ページ | Programming Place Plus 新C++編

トップページ新C++編完全転送

このページの概要 🔗

このページは、練習問題の解答例や解説のページです。



解答・解説 🔗

問題1 (確認★) 🔗

次のコード内にある T&&U&& はそれぞれ rvalueリファレンス、forwardingリファレンスのどちらですか?

template <typename T>
void func1(T&& v) {
}

template <typename T>
class MyClass {
public:
    void func2(T&& v) {
    }

    template <typename U>
    void func3(U&& v) {
    }
};


rvalueリファレンスと forwardingリファレンスはいずれも && を使いますが、テンプレート仮引数や auto のように、型推論が関与する場合に forwardingリファレンスとして扱われます。しかし、T&& のような記述のすべてが forwardingリファレンスになるわけではないことに注意が必要です(本編解説)。

func1(T&& v)T&& は forwardingリファレンスです。

MyClass::func2(T&& v)T&& は rvalueリファレンス です。T はクラステンプレートのテンプレート仮引数であり、クラステンプレートをインスタンス化するときに決定済みであるからです。MyClass<int> のようにインスタンス化したなら、void func2(int&& v) となっているため、これは rvalueリファレンスです。

MyClass::func3(U&& v)U&& は forwardingリファレンスです。func2 の場合と異なり、クラステンプレートのテンプレート仮引数ではなく、このメンバ関数固有のテンプレート仮引数であるため、型推論が関与することになります。

問題2 (確認★) 🔗

型推論により T が以下のように推論された場合、T&& は最終的にどのような型になりますか?

  1. T が int& に推論された場合
  2. T が int に推論された場合
  3. T が const int& に推論された場合


1番。T が int& に推論された場合、T&&int& && です。参照の圧縮のルールにより、&&&& に畳まれるため、最終的には int& になります(本編解説)。

2番。T が int に推論された場合、T&&int&& です。

3番。T が const int& に推論された場合、T&&const int& && です。参照の圧縮のルールにより、&&&& に畳まれるため、最終的には const int& になります。

問題3 (基本★) 🔗

次のプログラムの wrapper関数内に、完全転送のコードを実装してください。

#include <iostream>
#include <utility>

void process(int& v) {
    std::cout << "lvalue\n";
}

void process(int&& v) {
    std::cout << "rvalue\n";
}

template <typename T>
void wrapper(T&& v) {
    // ここに完全転送の実装を追加
}

int main()
{
    int x = 10;
    wrapper(x);          // "lvalue" と出力
    wrapper(20);         // "rvalue" と出力
}


完全転送を行うには、仮引数に forwardingリファレンスを使用し、関数内で std::forward関数を使って引数を転送します(本編解説)。以下のように実装できます。

template <typename T>
void wrapper(T&& v) {
    process(std::forward<T>(v));
}

プログラム全体は以下のようになります。

#include <iostream>
#include <utility>

void process(int& v) {
    std::cout << "lvalue\n";
}

void process(int&& v) {
    std::cout << "rvalue\n";
}

template <typename T>
void wrapper(T&& v) {
    process(std::forward<T>(v));
}

int main()
{
    int x = 10;
    wrapper(x);          // "lvalue" と出力
    wrapper(20);         // "rvalue" と出力
}

実行結果:

lvalue
rvalue

問題4 (調査★★) 🔗

直接配置による方法と、push_backメンバ関数などを使ってオブジェクトを作って渡す方法の実行速度の違いを計測してください。


std::chrono を使って、それぞれの場合の実行時間を計測して比較します(「chrono」のページを参照)。

#include <chrono>
#include <iostream>
#include <string>
#include <vector>

class MyData {
public:
    MyData(int a, const std::string& b)
        : m_a {a}, m_b {b}
    {
    }

private:
    int m_a;
    std::string m_b;
};

int main()
{
    const int iterations {1000000};
    
    // 直接配置(emplace_back)
    auto begin = std::chrono::steady_clock::now();
    {
        std::vector<MyData> vec {};
        for (int i = 0; i < iterations; ++i) {
            vec.emplace_back(i, "test");
        }
    }
    auto end = std::chrono::steady_clock::now();
    std::cout << "emplace_back: " 
              << std::chrono::duration_cast<std::chrono::milliseconds>(end - begin).count() 
              << "ms\n";
    
    // push_back
    begin = std::chrono::steady_clock::now();
    {
        std::vector<MyData> vec {};
        for (int i = 0; i < iterations; ++i) {
            vec.push_back(MyData(i,"test"));
        }
    }
    end = std::chrono::steady_clock::now();
    std::cout << "push_back: " 
              << std::chrono::duration_cast<std::chrono::milliseconds>(end - begin).count() 
              << "ms\n";
}

実行結果:

emplace_back: 7840ms
push_back: 9436ms

emplace_backメンバ関数を使う方法では、いったんオブジェクトを生成したうえでコピーすることになります。直接配置により、一時オブジェクトの生成とコピー(やムーブ)を避けられることで、実行時間が短くなる可能性があります。ただし、いつもそうですが、本当にどちらが優位になるかは、コンパイラの違いや最適化の結果、実行環境やデータの違いなどさまざまな要因によります。実際に計測してみることが重要です。


参考リンク 🔗



更新履歴 🔗




はてなブックマーク に保存 Facebook でシェア X で ポストフォロー
LINE で送る noteで書く rss1.0 取得ボタン RSS
管理者情報 プライバシーポリシー
先頭へ戻る