Menu
Coddy logo textTech

完全転送

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

引数を受け取り、それらを他の関数に pass するテンプレート関数を書くと、問題が生じます。元の引数が lvalue だったのか rvalue だったのかを、どのように保持すればよいのでしょうか。完全な forwardingは、引数を受け取ったときとまったく同じ状態で渡し、その値カテゴリーを維持することで、この問題を解決します。

主な要素は forwarding 参照(テンプレートの文脈では T&& と記述)と std::forward です。forwarding 参照は lvalue と rvalue の両方にバインドでき、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&) を呼び出す - 左辺値が保持される
    wrapper(20);      // process(int&&) を呼び出す - 右辺値が保持される
}

std::forward がない場合、名前付きの parameter は、rvalue が渡された場合でも、function 内では常に lvalue になります。これにより、Constructor やその他の function に forwarding する際に、move セマンティクスが正しく機能しなくなります。

完全転送は、ファクトリー function、ラッパー class、そして move と copy のどちらを行うかという呼び出し元の意図を保持しながらオブジェクトを構築したり function を呼び出したりする必要がある、あらゆる汎用コードを書くうえで不可欠です。

challenge icon

チャレンジ

簡単

引数として渡された値カテゴリを保持しながらオブジェクトを構築することで、完全転送を示すファクトリ関数を作成しましょう。引数をクラスコンストラクターに転送する汎用的な make_object 関数を作成し、lvalue はコピーされ、rvalue は移動されるようにします。

コードを3つのファイルに分けて整理します。

  • Widget.h: データの受け取り方を追跡する Widget クラスを定義します。

    Widget には std::string の name と int の value を保持させます。2つのコンストラクターを用意します。

    • const std::string&(lvalue reference)と int を受け取るもの:Widget constructed (copy): [name] を出力します
    • std::string&&(rvalue reference)と int を受け取るもの:Widget constructed (move): [name] を出力し、文字列を移動します

    さらに、[name]: [value] を出力する display() method を追加します。

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

    name に対する forwarding reference と、value に対する通常の int を受け取るテンプレート function make_object を記述します。std::forward を使用して、元の値カテゴリを保持したまま name 引数を Widget コンストラクターに渡します。

    この function は、構築した Widget を値として返します。

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

    2つの widget を作成して、完全転送を実演します。

    1. まず、name を local variable に保存し、lvalue として make_object に渡します
    2. 次に、一時的な文字列(同じ name に "_temp" を連結したもの)を rvalue として、直接 make_object に渡します

    各 widget を作成した後、その display() method を呼び出します。

たとえば、入力が 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 function 自体は、lvalue を受け取ったのか rvalue を受け取ったのかを認識していないということです。しかし、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 を左辺値として渡して最初の widget を作成する
    // name をローカル変数に格納し、make_object に渡す
    
    
    // TODO: 最初の widget で display() を呼び出す
    
    
    // TODO: 一時的な文字列を右辺値として渡して2番目の widget を作成する
    // (name + "_temp") を直接 make_object に渡す
    
    
    // TODO: 2番目の widget で display() を呼び出す
    
    
    return 0;
}
quiz icon腕試し

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

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

自分で練習してみよう: C++オンラインコンパイラ