完美转发
本课属于 Coddy C++ 学习旅程的“面向对象编程”单元。第 84 课,共 104 课。
编写接收参数并将其传递给其他函数的模板函数时,会遇到一个问题:如何保留原始参数是左值还是右值?完美转发可以解决这个问题,它会按照接收到参数时的原样传递参数,并保留参数的值类别。
关键要素是转发引用(在模板上下文中写作 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&),保留左值属性
wrapper(20); // 调用 process(int&&),保留右值属性
}如果没有 std::forward,命名参数在函数内部始终是左值,即使传入的是右值也是如此。这样一来,转发给构造函数或其他函数时,移动语义就无法正常工作。
完美转发对于编写工厂函数、包装类以及任何需要构造对象或调用函数,同时保留调用方对于移动还是复制的意图的泛型代码至关重要。
挑战
简单我们来编写一个工厂函数,通过构造对象来演示完美转发,同时保留传入参数的值类别。你将创建一个通用的 make_object 函数,把参数转发给类构造函数,确保左值会被复制,右值会被移动。
你需要将代码分成三个文件:
Widget.h:定义一个Widget类,用于记录它接收数据的方式。你的
Widget应存储一个std::string类型的名称和一个int类型的值。提供两个构造函数:- 一个接收
const std::string&(左值引用)和int的构造函数:输出Widget constructed (copy): [name] - 一个接收
std::string&&(右值引用)和int的构造函数:输出Widget constructed (move): [name],并移动字符串
另外,添加一个
display()方法,输出[name]: [value]。- 一个接收
Factory.h:创建完美转发工厂函数。编写一个模板函数
make_object,它接收一个用于名称的转发引用,以及一个普通的int类型的值。使用std::forward将名称参数传给Widget构造函数,同时保留其原始值类别。该函数应按值返回构造好的
Widget。main.cpp:读取两个输入:- 一个名称(字符串)
- 一个值(整数)
通过创建两个 widget 来演示完美转发:
- 首先,将名称存储在局部变量中,并将它作为左值传给
make_object - 其次,将一个临时字符串(将相同名称与
"_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 构造函数时会保留这一信息。这样,构造函数重载解析就能选择合适的版本,让临时对象高效地执行移动操作,同时安全地复制具名变量。
亲手试试
#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: 将临时字符串作为右值传入,创建第二个 widget
// 将 (name + "_temp") 直接传给 make_object
// TODO: 对第二个 widget 调用 display()
return 0;
}
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
面向对象编程中的所有课
自己练习: C++ 在线编译器