跳至内容

完美转发与 std::forward

理解为什么具名的右值引用是左值,学习使用 std::forward 的规范转发模式,以及 emplace_back 如何使用可变参数转发来就地构造对象。

问题:具名的右值引用是左值

这是完美转发所解决的根本问题:当你以 T&& 接受参数并传递给另一个函数时,名字使其成为左值:

void inner(int& x)  { std::cout << "lvalue\n"; }
void inner(int&& x) { std::cout << "rvalue\n"; }

void outer(int&& x) {
    inner(x);  // 调用 inner(int&) ── x 是左值!
}

值类别在每个函数边界处丢失。如果你在编写一个包装器函数(日志、计时、工厂),你需要在保留原始值类别的同时转发参数。简单地传递 x 总是传递左值。使用 std::move(x) 总是传递右值。对于泛型包装器来说,两者都不正确。

std::forward:条件转换

std::forward(x) 是一个条件转换:

- 如果 T 是左值引用(因为传入了左值而被推导),std::forward(x) 返回左值引用 ── 不做转换。
- 如果 T 是非引用(因为传入了右值而被推导),std::forward(x) 返回右值引用 ── 转换为 T&&。

与 std::move 的关键区别:
- std::move 总是转换为右值(无条件)。
- std::forward 仅在原始参数是右值时转换为右值(有条件)。

std::forward 应该仅与转发引用(T&&,其中 T 是被推导的)一起使用。在其他地方使用它是一个 bug。

规范转发模式

模式:接受一个转发引用 T&&,然后使用 std::forward(arg) 在传递给另一个函数时保留值类别。

perfect_forwarding.cpp
#include <iostream>
#include <string>
#include <utility>
#include <chrono>

// The function we want to wrap
void process(const std::string& s) {
    std::cout << "process(lvalue): " << s << '\n';
}
void process(std::string&& s) {
    std::cout << "process(rvalue): " << s << '\n';
    // Can steal s's resources since it's an rvalue
}

// Generic wrapper that perfectly forwards
template <typename T>
void timed_process(T&& arg) {
    auto start = std::chrono::steady_clock::now();

    // std::forward<T>(arg) preserves the original value category
    process(std::forward<T>(arg));

    auto elapsed = std::chrono::steady_clock::now() - start;
    std::cout << "Took: " << elapsed.count() << " ns\n";
}

int main() {
    std::string name = "Alice";
    timed_process(name);              // T = string&,  forwards as lvalue
    timed_process(std::string("Bob")); // T = string, forwards as rvalue
    timed_process("Charlie");          // T = const char(&)[8], forwards as lvalue
}

可变参数转发与 emplace_back

完美转发在可变参数模板中真正大放异彩。emplace_back 将构造函数参数直接转发到容器中,就地构造对象,无需任何拷贝或移动。

variadic_forwarding.cpp
#include <iostream>
#include <vector>
#include <string>
#include <utility>

struct Employee {
    std::string name;
    int age;
    Employee(std::string n, int a) : name(std::move(n)), age(a) {
        std::cout << "Constructed: " << name << '\n';
    }
    Employee(const Employee&) { std::cout << "Copied\n"; }
    Employee(Employee&&) noexcept { std::cout << "Moved\n"; }
};

// Custom factory using variadic forwarding
template <typename T, typename... Args>
std::vector<T> make_vector_of(Args&&... args) {
    std::vector<T> v;
    // Fold expression (C++17) + emplace_back
    (v.emplace_back(std::forward<Args>(args)), ...);
    return v;
}

int main() {
    std::vector<Employee> team;

    // push_back: creates temporary, then MOVES it into vector
    team.push_back(Employee("Alice", 30));
    // Output: Constructed: Alice, Moved

    // emplace_back: constructs IN-PLACE — no copy, no move
    team.emplace_back("Bob", 25);
    // Output: Constructed: Bob

    std::cout << "---\n";
    // Variadic forwarding
    auto v = make_vector_of<std::string>("hello", std::string("world"), "!");
    for (const auto& s : v) std::cout << s << ' ';
    std::cout << '\n';
}

move vs forward:了解区别

| | std::move | std::forward |
|---|---|---|
| 转换类型 | 无条件转换为 T&& | 有条件 ── 如果 T 是 T& 则为左值,如果 T 是 T 则为右值 |
| 使用场景 | 「我用完了,拿走它」 | 「带着原始值类别传递下去」 |
| 上下文 | 具体类型,变量的最后一次使用 | 仅限模板转发引用 |
| 风险 | 意外移动了仍然需要的值 | 在没有转发引用的情况下使用(无意义) |

经验法则:当你拥有值并想要放弃它时使用 std::move。当你在转发别人的值时使用 std::forward。

常见陷阱

1. 多次转发:std::forward(arg) 可能转换为右值。如果你将同一参数转发给两个函数,第一个可能窃取其资源,给第二个留下垃圾。每个参数最多转发一次。

2. auto&& 是转发引用:for (auto&& elem : container) 推导值类别。这在 range-for 中没问题,但在其他上下文中使用 auto&& 时要小心。

3. 没有推导的 std::forward:std::forward(s) 只是伪装的 std::move(s)。只将 std::forward 与被推导的模板参数一起使用。

4. 花括号初始化列表:{1, 2, 3} 没有类型,不能被转发。先赋值给变量。

最佳实践

1. 仅将 std::forward 与转发引用一起使用(T&&,其中 T 是被推导的)。
2. 每个参数最多转发一次 ── 转发可能移动,使源对象变空。
3. 在容器中构造对象时优先使用 emplace_back 而非 push_back。
4. 不要在循环中转发 ── 第一次迭代可能窃取参数。
5. 当需要对左值和右值有不同行为时,使用 if constexpr 配合 std::is_lvalue_reference_v。

关键要点
  • 具名的右值引用是左值 ── 值类别在函数边界处丢失
  • std::forward 有条件地转换:左值保持左值,右值保持右值
  • std::move 是无条件的(总是右值);std::forward 是有条件的(保留原始值类别)
  • emplace_back 使用可变参数转发就地构造对象,零拷贝
  • 每个参数最多转发一次 ── 转发可能窃取资源

测验 — 检验你的知识

(20 经验值)

1. `std::move` 和 `std::forward` 之间有什么区别?

2. 为什么 `emplace_back` 比 `push_back` 在构造元素时更高效?

3. 如果你将同一个参数 `std::forward` 给两个不同的函数会怎样?