Menu
Coddy logo textTech

完全転送

CoddyのC++ジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 84/104。

引数を受け取って他の関数に渡すテンプレート関数を記述する際、問題が発生します。元の引数が左辺値(lvalue)であったか右辺値(rvalue)であったかをどのように保持すればよいでしょうか? 完全転送(Perfect forwarding)は、引数を受け取ったときとまったく同じ状態で渡すことで値カテゴリを維持し、この問題を解決します。

主要な要素はフォワーディング参照(テンプレートコンテキストでは T&& と記述されます)および std::forward です。フォワーディング参照は左辺値と右辺値の両方にバインドでき、std::forward は引数を元の型に条件付きでキャストします。

#include <iostream>
#include <utility>

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

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

int main() {
    int n = 10;
    wrapper(n);       // process(int&) を呼び出す - 左辺値(lvalue)が保持される
    wrapper(20);      // process(int&&) を呼び出す - 右辺値(rvalue)が保持される
}

std::forward がないと、たとえ rvalue が渡されたとしても、名前付きパラメータは関数内では常に lvalue となります。これにより、コンストラクタや他の関数に転送する際に、ムーブセマンティクスが正しく機能しなくなります。

パーフェクトフォワーディングは、ファクトリ関数やラッパークラス、およびムーブかコピーかに関する呼び出し側の意図を保持しながらオブジェクトを構築したり関数を呼び出したりする必要があるジェネリックなコードを記述する際に不可欠です。

challenge icon

チャレンジ

簡単

引数の値カテゴリを維持しながらオブジェクトを構築することで、完全転送(Perfect Forwarding)を実証するファクトリ関数を作成しましょう。引数をクラスのコンストラクタに転送し、左辺値(lvalue)はコピーされ、右辺値(rvalue)はムーブされることを保証する汎用的な make_object 関数を作成します。

コードは以下の3つのファイルに分けて構成します:

  • Widget.h: どのようにデータを受け取ったかを追跡する Widget クラスを定義します。

    Widgetstd::string 型の name と int 型の value を保持する必要があります。2つのコンストラクタを用意してください:

    • const std::string&(左辺値参照)と int を受け取るもの — Widget constructed (copy): [name] と出力します。
    • std::string&&(右辺値参照)と int を受け取るもの — Widget constructed (move): [name] と出力し、文字列をムーブします。

    また、[name]: [value] と出力する display() メソッドを追加してください。

  • Factory.h: 完全転送を行うファクトリ関数を作成します。

    name には転送参照(forwarding reference)を、value には通常の int を受け取るテンプレート関数 make_object を記述します。std::forward を使用して、元の値カテゴリを維持したまま name 引数を Widget コンストラクタに渡します。

    この関数は、構築された Widget を値で返す必要があります。

  • main.cpp: 2つの入力を読み取ります:
    1. 名前(文字列)
    2. 値(整数)

    2つの Widget を作成することで完全転送を実証します:

    1. 1つ目は、名前をローカル変数に格納し、それを左辺値として make_object に渡します。
    2. 2つ目は、一時的な文字列(同じ名前に "_temp" を連結したもの)を右辺値として直接 make_object に渡します。

    各 Widget を作成した後、その display() メソッドを呼び出します。

例えば、入力が Gadget42 の場合:

Widget constructed (copy): Gadget
Gadget: 42
Widget constructed (move): Gadget_temp
Gadget_temp: 42

入力が Device100 の場合:

Widget constructed (copy): Device
Device: 100
Widget constructed (move): Device_temp
Device_temp: 100

重要なポイントは、make_object 関数自体は左辺値を受け取ったのか右辺値を受け取ったのかを知りませんが、std::forward を使用することで、Widget コンストラクタを呼び出す際にその情報を保持できるということです。これにより、コンストラクタのオーバーロード解決が適切なバージョンを選択し、一時オブジェクトに対しては効率的なムーブを可能にし、名前付き変数に対しては安全なコピーを可能にします。

自分で試してみよう

#include <iostream>
#include <string>
#include "Widget.h"
#include "Factory.h"

using namespace std;

int main() {
    // 入力を読み込む
    string name;
    int value;
    cin >> name >> value;
    
    // TODO: nameを左辺値として渡して、最初のウィジェットを作成する
    // 名前をローカル変数に格納し、それをmake_objectに渡す
    
    
    // TODO: 最初のウィジェットでdisplay()を呼び出す
    
    
    // TODO: 一時的な文字列を右辺値として渡して、2番目のウィジェットを作成する
    // (name + "_temp") を直接 make_object に渡す
    
    
    // TODO: 2番目のウィジェットでdisplay()を呼び出す
    
    
    return 0;
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン