Menu
CoddyTech

完美转发

本课属于 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,命名参数在函数内部始终是左值,即使传入的是右值也是如此。这样一来,转发给构造函数或其他函数时,移动语义就无法正常工作。

完美转发对于编写工厂函数、包装类以及任何需要构造对象或调用函数,同时保留调用方对于移动还是复制的意图的泛型代码至关重要。

challenge icon

挑战

简单

我们来编写一个工厂函数,通过构造对象来演示完美转发,同时保留传入参数的值类别。你将创建一个通用的 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:读取两个输入:
    1. 一个名称(字符串)
    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 构造函数时会保留这一信息。这样,构造函数重载解析就能选择合适的版本,让临时对象高效地执行移动操作,同时安全地复制具名变量。

亲手试试

#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;
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: C++ 在线编译器