完全転送
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 を呼び出したりする必要がある、あらゆる汎用コードを書くうえで不可欠です。
チャレンジ
簡単引数として渡された値カテゴリを保持しながらオブジェクトを構築することで、完全転送を示すファクトリ関数を作成しましょう。引数をクラスコンストラクターに転送する汎用的な 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を受け取るテンプレート functionmake_objectを記述します。std::forwardを使用して、元の値カテゴリを保持したまま name 引数をWidgetコンストラクターに渡します。この function は、構築した
Widgetを値として返します。main.cpp: 2つの入力を読み取ります。- name(文字列)
- value(整数)
2つの widget を作成して、完全転送を実演します。
- まず、name を local variable に保存し、lvalue として
make_objectに渡します - 次に、一時的な文字列(同じ name に
"_temp"を連結したもの)を rvalue として、直接make_objectに渡します
各 widget を作成した後、その
display()method を呼び出します。
たとえば、入力が Gadget と 42 の場合:
Widget constructed (copy): Gadget
Gadget: 42
Widget constructed (move): Gadget_temp
Gadget_temp: 42入力が Device と 100 の場合:
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;
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
3コンストラクタとデストラクタ
デフォルトコンストラクタパラメータ付きコンストラクタコピーコンストラクタムーブコンストラクタコンストラクタ初期化リスト委譲コンストラクタデストラクタ徹底解説Rule of Three / Five / Zero復習 - Stringクラス6演算子オーバーロード
演算子オーバーロード入門算術演算子のオーバーロード比較演算子のオーバーロードストリーム演算子代入演算子のオーバーロード[] と () の演算子オーバーロード型変換演算子復習 - Matrixクラス自分で練習してみよう: C++オンラインコンパイラ