完全転送
CoddyのC++ジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 84/104。
引数を受け取り、それを他の関数に渡すテンプレート関数を記述する際、ある問題が発生します。それは、元の引数が左辺値(lvalue)であったか右辺値(rvalue)であったかをどのように保持するかという点です。パーフェクトフォワーディング(Perfect forwarding)は、引数を受け取った通りの状態で渡すことで、その値カテゴリを維持し、この問題を解決します。
重要な要素は、転送参照 (forwarding references)(テンプレートコンテキストにおいて 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 がないと、たとえ右辺値が渡されたとしても、名前付きパラメータは関数内では常に左辺値となります。これにより、コンストラクタや他の関数に転送する際に、ムーブセマンティクスが正しく動作しなくなります。
パーフェクトフォワーディングは、ファクトリ関数やラッパークラス、およびムーブかコピーかに関する呼び出し側の意図を保持しながらオブジェクトを構築したり関数を呼び出したりする必要があるあらゆるジェネリックなコードを記述する際に不可欠です。
チャレンジ
簡単渡された引数の値カテゴリを維持しながらオブジェクトを構築することで、完全転送(perfect forwarding)を実証するファクトリ関数を作成しましょう。引数をクラスのコンストラクタに転送する汎用的な make_object 関数を作成し、左辺値(lvalue)はコピーされ、右辺値(rvalue)はムーブされるようにします。
コードは以下の3つのファイルに分けて構成します:
Widget.h: データの受け取り方を追跡するWidgetクラスを定義します。Widgetはstd::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つの入力を読み取ります:- 名前(文字列)
- 値(整数)
2つの widget を作成することで完全転送を実証します:
- 1つ目は、名前をローカル変数に格納し、それを左辺値として
make_objectに渡します。 - 2つ目は、一時的な文字列(同じ名前に
"_temp"を連結したもの)を右辺値として直接make_objectに渡します。
各 widget を作成した後、その
display()メソッドを呼び出します。
例えば、入力が 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 関数自体は左辺値を受け取ったのか右辺値を受け取ったのかを意識しませんが、std::forward を使用することで、Widget コンストラクタを呼び出す際にその情報を保持できるという点です。これにより、コンストラクタのオーバーロード解決が適切なバージョンを選択し、一時オブジェクトに対しては効率的なムーブを可能にし、名前付き変数に対しては安全なコピーを可能にします。
チートシート
パーフェクトフォワーディング(Perfect forwarding)を使用すると、テンプレート関数は引数の元の値カテゴリ(左辺値または右辺値)を保持したまま、他の関数に引数を渡すことができます。
この仕組みは、転送参照(forwarding references)(テンプレートコンテキストにおける T&&)と std::forward を組み合わせて使用します:
#include <utility>
template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));
}転送参照は、左辺値と右辺値の両方にバインドできます。std::forward<T> は、引数を元の型に条件付きでキャストします。
std::forward がないと、関数内の名前付きパラメータは、たとえ右辺値が渡されたとしても、常に左辺値として扱われます。これにより、ムーブセマンティクスが正しく機能しなくなります。
値カテゴリの保持を示す例:
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 n = 10;
wrapper(n); // process(int&) を呼び出す - 左辺値が保持される
wrapper(20); // process(int&&) を呼び出す - 右辺値が保持されるパーフェクトフォワーディングは、ファクトリ関数、ラッパークラス、およびムーブとコピーに関する呼び出し側の意図を保持しながらオブジェクトを構築したり関数を呼び出したりするジェネリックなコードにとって不可欠です。
自分で試してみよう
#include <iostream>
#include <string>
#include "Widget.h"
#include "Factory.h"
using namespace std;
int main() {
// 入力を読み込む
string name;
int value;
cin >> name >> value;
// TODO: name を左辺値 (lvalue) として渡して、1つ目の widget を作成する
// name をローカル変数に格納し、それを make_object に渡す
// TODO: 1つ目の widget に対して display() を呼び出す
// TODO: 一時的な文字列を右辺値 (rvalue) として渡して、2つ目の widget を作成する
// (name + "_temp") を直接 make_object に渡す
// TODO: 2つ目の widget に対して display() を呼び出す
return 0;
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
3コンストラクタとデストラクタ
デフォルトコンストラクタ引数付きコンストラクタコピーコンストラクタムーブコンストラクタコンストラクタ初期化リスト委譲コンストラクタデストラクタ詳解Rule of Three / Five / Zeroまとめ - Stringクラス6演算子オーバーロード
演算子オーバーロード入門算術演算子のオーバーロード比較演算子のオーバーロードストリーム演算子代入演算子のオーバーロード[] と () 演算子のオーバーロード型変換演算子まとめ:Matrix クラス