概要
完全転送 (perfect forwarding) はC++11で導入された機能であり、関数の引数を他の関数に転送する時に、引数の値カテゴリ (左辺値・右辺値) と 型修飾 (const等) を保持する技術である。
ユニバーサル参照とstd::forwardを組み合わせることで実現される。
完全転送は、C++の高度な機能であり、以下に示す事柄を理解する必要がある。
- ユニバーサル参照 と
std::forwardをセットで使用する。 - 引数の値カテゴリと型修飾を保持したまま転送できる。
std::moveは無条件に右辺値にキャストするが、std::forwardは条件付き。- 可変長引数テンプレートと組み合わせることにより、柔軟性が向上する。
- 標準ライブラリの多くの関数で使用されている。
完全転送を適切に使用することで、効率的で保守性の高いコードを書くことができる。
完全転送の必要性
背景
関数の引数を別の関数に転送する時、従来の方法では値カテゴリの情報が失われる問題があった。
この問題を解決するために、完全転送が必要となる。
#include <iostream>
// 左辺値参照
void target(int& x)
{
std::cout << "Lvalue reference version" << std::endl;
}
// 右辺値参照
void target(int&& x)
{
std::cout << "Rvalue reference version" << std::endl;
}
// 不完全な転送の例
template<typename T>
void forwardBad(T&& param)
{
// paramは名前を持つため、常に左辺値として扱われる
target(param); // 常に左辺値参照が呼ばれる
}
int main()
{
int x = 10;
forwardBad(x); // 左辺値を渡す -> 左辺値参照が呼ばれる
forwardBad(20); // 右辺値を渡す -> 左辺値参照が呼ばれる (問題)
forwardBad(std::move(x)); // 右辺値を渡す -> 左辺値参照が呼ばれる (問題)
return 0;
}
std::forwardの役割
基本的な動作
std::forwardは、引数の値カテゴリを保持したまま転送するためのキャスト関数である。
- 左辺値として渡された引数は、左辺値として転送される。
- 右辺値として渡された引数は、右辺値として転送される。
std::forwardの使用例
#include <iostream>
#include <utility>
// 左辺値参照
void target(int& x)
{
std::cout << "Lvalue reference version: " << x << std::endl;
}
// 右辺値参照
void target(int&& x)
{
std::cout << "Rvalue reference version: " << x << std::endl;
}
// 完全転送の実装
template<typename T>
void forwardGood(T&& param)
{
// std::forwardを使用して値カテゴリを保持
target(std::forward<T>(param));
}
int main()
{
int x = 10;
forwardGood(x); // 左辺値を渡す -> 左辺値参照が呼ばれる
forwardGood(20); // 右辺値を渡す -> 右辺値参照が呼ばれる
forwardGood(std::move(x)); // 右辺値を渡す -> 右辺値参照が呼ばれる
return 0;
}
std::forwardの仕組み
内部定義の概念
std::forwardは、参照の折り畳み規則と型推論を利用している。
// std::forwardの簡略化された定義のイメージ
// 左辺値参照
template<typename T>
T&& forward(typename std::remove_reference<T>::type& param)
{
return static_cast<T&&>(param);
}
// 右辺値参照
template<typename T>
T&& forward(typename std::remove_reference<T>::type&& param)
{
return static_cast<T&&>(param);
}
型推論との組み合わせ
template<typename T>
void wrapper(T&& param)
{
// ケース1 : 左辺値を渡した場合
// T = int&
// std::forward<int&>(param) → static_cast<int& &&>(param)
// 参照の折り畳み: int& && → int&
// 結果: 左辺値参照
// ケース2: 右辺値を渡した場合
// T = int
// std::forward<int>(param) → static_cast<int&&>(param)
// 結果: 右辺値参照
target(std::forward<T>(param));
}
完全転送の例
ファクトリ関数の実装
#include <iostream>
#include <memory>
#include <string>
#include <utility>
class Product
{
private:
std::string name;
int id;
double price;
public:
Product(const std::string& n, int i, double p) : name(n), id(i), price(p)
{
std::cout << "Product created (lvalue string): " << name << std::endl;
}
Product(std::string&& n, int i, double p) : name(std::move(n)), id(i), price(p)
{
std::cout << "Product created (rvalue string): " << name << std::endl;
}
void display() const
{
std::cout << "Product: " << name << ", ID: " << id << ", Price: " << price << std::endl;
}
};
// 完全転送を使用したファクトリ関数
template<typename... Args>
std::unique_ptr<Product> createProduct(Args&&... args)
{
return std::make_unique<Product>(std::forward<Args>(args)...);
}
int main()
{
std::string name1 = "Widget";
// 左辺値を転送
auto p1 = createProduct(name1, 100, 29.99);
// 右辺値を転送
auto p2 = createProduct(std::string("Gadget"), 200, 49.99);
// 混在
auto p3 = createProduct("Tool", 300, 19.99);
p1->display();
p2->display();
p3->display();
return 0;
}
ラッパークラスの実装
#include <iostream>
#include <utility>
#include <vector>
// 要素を保持するコンテナ
template<typename T>
class Container
{
private:
std::vector<T> data;
public:
// 完全転送を使用した要素追加
template<typename U>
void add(U&& item)
{
std::cout << "Adding item..." << std::endl;
data.push_back(std::forward<U>(item));
}
// 完全転送を使用した構築
template<typename... Args>
void emplace(Args&&... args)
{
std::cout << "Emplacing item..." << std::endl;
data.emplace_back(std::forward<Args>(args)...);
}
void display() const
{
for (const auto& item : data)
{
std::cout << item << " ";
}
std::cout << std::endl;
}
};
int main()
{
Container<std::string> container;
std::string str = "Hello";
container.add(str); // 左辺値 : コピー
container.add(std::string("World")); // 右辺値 : ムーブ
container.add(std::move(str)); // ムーブ
container.emplace(5, 'X'); // 直接構築
container.display();
return 0;
}
コールバック関数の転送
#include <iostream>
#include <functional>
#include <utility>
// コールバックを実行する関数
template<typename Func, typename... Args>
auto executeCallback(Func&& func, Args&&... args)
-> decltype(std::forward<Func>(func)(std::forward<Args>(args)...))
{
std::cout << "Executing callback..." << std::endl;
return std::forward<Func>(func)(std::forward<Args>(args)...);
}
// テスト用の関数
int add(int a, int b)
{
return a + b;
}
void printMessage(const std::string& msg)
{
std::cout << "Message: " << msg << std::endl;
}
int main()
{
// 関数ポインタの転送
int result1 = executeCallback(add, 10, 20);
std::cout << "Result: " << result1 << std::endl;
// ラムダ式の転送
auto multiply = [](int a, int b) { return a * b; };
int result2 = executeCallback(multiply, 5, 6);
std::cout << "Result: " << result2 << std::endl;
// std::functionの転送
std::function<void(const std::string&)> func = printMessage;
executeCallback(func, "Hello, World!");
// 一時的なラムダ式(右辺値)の転送
executeCallback([](int x) { std::cout << "Value: " << x << std::endl; }, 42);
return 0;
}
可変長引数テンプレートとの組み合わせ
基本パターン
完全転送は、可変長引数テンプレートと組み合わせることが多い。
#include <iostream>
#include <utility>
// 再帰の終端
void print()
{
std::cout << std::endl;
}
// 可変長引数と完全転送
template<typename T, typename... Args>
void print(T&& first, Args&&... rest)
{
std::cout << std::forward<T>(first) << " ";
print(std::forward<Args>(rest)...);
}
int main()
{
print(1, 2.5, "Hello", 'A');
std::string str = "World";
print(str, 42, std::string("Test"));
return 0;
}
タプルとの組み合わせ
#include <iostream>
#include <tuple>
#include <utility>
#include <string>
// タプルを構築する関数
template<typename... Args>
auto makeTupleForward(Args&&... args)
{
return std::make_tuple(std::forward<Args>(args)...);
}
int main()
{
std::string str = "Hello";
int value = 42;
// 完全転送を使用してタプルを構築
auto t1 = makeTupleForward(str, value, 3.14); // コピー
auto t2 = makeTupleForward(std::move(str), value, 3.14); // ムーブ
std::cout << std::get<0>(t1) << ", "
<< std::get<1>(t1) << ", "
<< std::get<2>(t1) << std::endl;
std::cout << std::get<0>(t2) << ", "
<< std::get<1>(t2) << ", "
<< std::get<2>(t2) << std::endl;
return 0;
}
std::move と std::forwardの違い
使い分け
std::move と std::forwardは似ているが、使用目的が異なる。
| 特性 | std::move | std::forward |
|---|---|---|
| 目的 | 無条件に右辺値にキャスト | 条件付きで右辺値にキャスト |
| 使用場面 | 左辺値を右辺値として扱いたい | 引数の値カテゴリを保持したい |
| テンプレート | 不要 | 必須(テンプレートパラメータを指定) |
| 型推論 | 自動 | テンプレート引数に依存 |
| 典型的な使用 | ムーブ操作 | 完全転送 |
比較例
#include <iostream>
#include <utility>
#include <string>
void process(std::string& str)
{
std::cout << "Lvalue: " << str << std::endl;
}
void process(std::string&& str)
{
std::cout << "Rvalue: " << str << std::endl;
}
template<typename T>
void useMove(T&& param)
{
// 常に右辺値として転送
process(std::move(param));
}
template<typename T>
void useForward(T&& param)
{
// 値カテゴリを保持して転送
process(std::forward<T>(param));
}
int main()
{
std::string lvalue = "Left";
std::cout << "--- Using std::move ---" << std::endl;
useMove(lvalue); // 右辺値として転送
useMove(std::string("Right")); // 右辺値として転送
std::cout << "\n--- Using std::forward ---" << std::endl;
useForward(lvalue); // 左辺値として転送
useForward(std::string("Right")); // 右辺値として転送
return 0;
}
完全転送の注意
テンプレートパラメータの指定忘れ
template<typename T>
void func(T&& param)
{
// 誤り : テンプレートパラメータを指定していない
// target(std::forward(param)); // コンパイルエラー
// 正しい : テンプレートパラメータを指定
target(std::forward<T>(param)); // OK
}
複数回の転送
std::forwardは、1度しか使用できない。
2回目以降は左辺値として扱われる。
#include <iostream>
#include <utility>
void process(int&& x)
{
std::cout << "Rvalue: " << x << std::endl;
}
template<typename T>
void doubleForward(T&& param)
{
process(std::forward<T>(param)); // OK : 右辺値として転送
// process(std::forward<T>(param)); // 危険 : 2回目の転送
}
int main()
{
doubleForward(10);
return 0;
}
初期化子リストの転送
初期化子リストは完全転送できない。
#include <vector>
#include <utility>
template<typename T, typename... Args>
T create(Args&&... args)
{
return T(std::forward<Args>(args)...);
}
int main()
{
// エラー : 初期化子リストは推論できない
// auto vec = create<std::vector<int>>({1, 2, 3, 4, 5});
// 回避策 : 明示的に初期化子リストを作成
auto vec = create<std::vector<int>>(std::initializer_list<int>{1, 2, 3, 4, 5});
return 0;
}
オーバーロード解決の問題
#include <iostream>
void func(int x)
{
std::cout << "int version" << std::endl;
}
void func(int& x)
{
std::cout << "int& version" << std::endl;
}
template<typename T>
void wrapper(T&& param)
{
// オーバーロード解決が予期しない結果になる可能性
func(std::forward<T>(param));
}
int main()
{
int x = 10;
wrapper(x); // どちらが呼ばれるか?
return 0;
}
完全転送の推奨される使用方法
推奨事項
- 常に
std::forwardにテンプレートパラメータを明示的に指定する。 - ユニバーサル参照 と
std::forwardはセットで使用する。 std::forwardは、引数を1回だけ転送する。- 可変長引数テンプレートと組み合わせて柔軟性を高める。
constやvolatile修飾にも注意を払う。
テンプレート関数の設計パターン
#include <utility>
// パターン1 : 単一引数の完全転送
template<typename T>
void singleArgument(T&& arg)
{
target(std::forward<T>(arg));
}
// パターン2 : 複数引数の完全転送
template<typename T1, typename T2>
void multipleArguments(T1&& arg1, T2&& arg2)
{
target(std::forward<T1>(arg1), std::forward<T2>(arg2));
}
// パターン3 : 可変長引数の完全転送
template<typename... Args>
void variadicArguments(Args&&... args)
{
target(std::forward<Args>(args)...);
}
// パターン4 : 戻り値も転送
template<typename Func, typename... Args>
auto callAndForward(Func&& func, Args&&... args)
-> decltype(std::forward<Func>(func)(std::forward<Args>(args)...))
{
return std::forward<Func>(func)(std::forward<Args>(args)...);
}
パフォーマンスへの影響
完全転送を使用することにより、以下に示すパフォーマンス向上が期待できる。
- 不要なコピーを完全に排除できる。
- ムーブセマンティクスを最大限活用できる。
- テンプレートコードのオーバーヘッドが最小化される。
- コンパイラの最適化が効きやすくなる。
パフォーマンス比較
#include <iostream>
#include <vector>
#include <chrono>
#include <utility>
class LargeObject
{
private:
std::vector<int> data;
public:
LargeObject(size_t size = 1000000) : data(size, 42) {}
LargeObject(const LargeObject& other) : data(other.data)
{
std::cout << "Copy constructor" << std::endl;
}
LargeObject(LargeObject&& other) noexcept : data(std::move(other.data))
{
std::cout << "Move constructor" << std::endl;
}
};
// 値渡し (コピーが発生)
void processByValue(LargeObject obj)
{
// 何らかの処理
// ...略
}
// 完全転送
template<typename T>
void processByForward(T&& obj)
{
// std::forwardで転送
processByValue(std::forward<T>(obj));
}
int main()
{
LargeObject obj1;
std::cout << "--- Without perfect forwarding ---" << std::endl;
processByValue(obj1); // コピーが発生
std::cout << "\n--- With perfect forwarding ---" << std::endl;
processByForward(LargeObject()); // ムーブが発生
return 0;
}
使用例
標準ライブラリでの使用
C++標準ライブラリの多くの関数で完全転送が使用されている。
- std::make_unique
- std::make_shared
- std::vector::emplace_back
- std::map::emplace
- std::thread::thread
スマートポインタの構築
#include <memory>
#include <iostream>
#include <string>
class Resource
{
private:
std::string name;
int value;
public:
Resource(std::string n, int v) : name(std::move(n)), value(v)
{
std::cout << "Resource created: " << name << std::endl;
}
~Resource()
{
std::cout << "Resource destroyed: " << name << std::endl;
}
};
int main()
{
std::string name = "Resource1";
// 完全転送を使用したスマートポインタの構築
auto ptr1 = std::make_unique<Resource>(name, 100);
auto ptr2 = std::make_unique<Resource>(std::string("Resource2"), 200);
auto ptr3 = std::make_shared<Resource>("Resource3", 300);
return 0;
}