Chuyển tiếp hoàn hảo & std::forward
Hiểu lý do tại sao các tham chiếu giá trị được đặt tên là giá trị, tìm hiểu mẫu chuyển tiếp chuẩn với std::forward và xem cách emplace_back sử dụng chuyển tiếp biến đổi để xây dựng các đối tượng tại chỗ.
Vấn đề: Tham chiếu giá trị được đặt tên là giá trị
Đây là vấn đề cơ bản mà tính năng chuyển tiếp hoàn hảo giải quyết: khi bạn chấp nhận một đối số dưới dạngT&&và chuyển nó sang một hàm khác, name sẽ biến đối số đó thành một giá trị:
void inner(int& x) { std::cout << "lvalue\n"; }
void inner(int&& x) { std::cout << "rvalue\n"; }
void outer(int&& x) {
inner(x); // calls inner(int&) — x is an lvalue!
}The value category is lost at every function boundary. If you are writing a wrapper function (logging, timing, factory), you need to forward the argument with its original value category preserved. Simply passing x always passes an lvalue. Using std::move(x) luôn chuyển một giá trị. Cả hai đều không đúng đối với một trình bao bọc chung.
std::forward: Diễn viên có điều kiện
std::forwardlà diễn viên có điều kiện:
- NếuTlà tham chiếu lvalue (được suy ra vì một giá trị đã được thông qua),std::forwardtrả về tham chiếu lvalue — không truyền.
- NếuTlà không tham chiếu (suy ra vì một giá trị đã được thông qua),std::forwardtrả về một tham chiếu giá trị — chuyển sangT&&.
Điểm khác biệt chính so vớistd::move:
-std::moveluôn chuyển thành giá trị (vô điều kiện).
-std::forwardchuyển sang giá trị chỉ khi đối số ban đầu là một giá trị (có điều kiện).
CHỈ nên sử dụngstd::forwardvới các tham chiếu chuyển tiếp (T&&trong đó suy raT). Sử dụng nó ở nơi khác là một lỗi.
Mẫu chuyển tiếp Canonical
Mẫu: chấp nhận tham chiếu chuyển tiếpT&&, sau đó sử dụngstd::forwardđể duy trì danh mục giá trị khi chuyển sang hàm khác.
#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
}Chuyển tiếp đa dạng & emplace_back
Khả năng chuyển tiếp hoàn hảo thực sự tỏa sáng với mẫu đa dạng.emplace_backchuyển tiếp các đối số của hàm tạo trực tiếp vào vùng chứa, xây dựng đối tượng tại chỗ mà không có bất kỳ bản sao hoặc di chuyển nào.
#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';
}tiến và tiến: Biết sự khác biệt
| |std::move|std::forward|
|---|---|---|
| Loại diễn viên | Truyền vô điều kiện tớiT&&| Có điều kiện - giá trị nếuTlàT&, giá trị giá trị nếuTlàT|
| Trường hợp sử dụng | "Tôi làm xong việc này rồi, ăn trộm đi" | "Chuyển cái này cùng với danh mục giá trị ban đầu của nó" |
| Bối cảnh | Các loại bê tông, công dụng cuối cùng của một biến | Chỉ tham chiếu chuyển tiếp mẫu |
| Nguy hiểm | Vô tình di chuyển một giá trị bạn vẫn cần | Sử dụng mà không có tham chiếu chuyển tiếp (vô nghĩa) |
Quy tắc chung: sử dụngstd::movekhi bạn sở hữu giá trị và muốn cho đi. Sử dụngstd::forwardkhi bạn chuyển tiếp giá trị của người khác.
1. Chuyển tiếp nhiều lần:std::forwardcó thể chuyển sang giá trị. Nếu bạn chuyển tiếp cùng một đối số cho hai hàm, hàm đầu tiên có thể lấy cắp tài nguyên của nó và để lại rác cho hàm thứ hai. Chuyển tiếp mỗi đối số tối đa một lần.
2.auto&&là tham chiếu chuyển tiếp:for (auto&& elem : container)suy ra loại giá trị. Điều này tốt cho phạm vi nhưng hãy cẩn thận khi sử dụngauto&&trong các ngữ cảnh khác.
3.std::forwardkhông bị khấu trừ:std::forwardchỉ làstd::move(s)ngụy trang. Chỉ sử dụngstd::forwardvới tham số mẫu được suy ra.
4. Danh sách khởi tạo giằng:{1, 2, 3}không có loại và không thể chuyển tiếp. Gán cho một biến đầu tiên.
1. Chỉ sử dụngstd::forwardvới các tham chiếu chuyển tiếp (T&&trong đóTđược suy ra).
2. Chuyển tiếp mỗi đối số nhiều nhất một lần — việc chuyển tiếp có thể di chuyển, để trống nguồn.
3. Ưu tiênemplace_backhơnpush_back khi xây dựng đối tượng trong vùng chứa.
4. Không chuyển tiếp theo vòng lặp — lần lặp đầu tiên có thể lấy mất đối số.
5. Sử dụngif constexprvớistd::is_lvalue_reference_v khi bạn cần hành vi khác nhau cho giá trị và giá trị.
- Tham chiếu giá trị được đặt tên là giá trị - danh mục giá trị bị mất ở ranh giới hàm
std::forwardphôi có điều kiện: lvalue giữ nguyên giá trị, rvalue giữ nguyên giá trịstd::movelà vô điều kiện (luôn có giá trị);std::forwardcó điều kiện (bảo quản bản gốc)emplace_backsử dụng chuyển tiếp biến đổi để xây dựng các đối tượng tại chỗ mà không có bản sao- Chuyển tiếp mỗi đối số tối đa một lần - chuyển tiếp có thể lấy cắp tài nguyên
Câu đố — Kiểm tra kiến thức
(20 XP)1. Sự khác biệt giữa`std::move`và`std::forward`là gì?
2. Tại sao`emplace_back`hiệu quả hơn`push_back`trong việc xây dựng các phần tử?
3. Điều gì xảy ra nếu bạn`std::forward`có cùng một đối số cho hai hàm khác nhau?