Chuyển tiếp hoàn hảo
Thuộc phần Lập trình hướng đối tượng trong hành trình C++ của Coddy. Bài học 84 trên 104.
Khi viết các hàm mẫu nhận đối số rồi truyền chúng cho các hàm khác, một vấn đề nảy sinh: làm thế nào để giữ nguyên việc đối số ban đầu là lvalue hay rvalue? Chuyển tiếp hoàn hảo (perfect forwarding) giải quyết vấn đề này bằng cách truyền đối số chính xác như khi nhận được, đồng thời giữ nguyên loại giá trị của chúng.
Thành phần chính là tham chiếu chuyển tiếp (được viết là T&& trong ngữ cảnh template) và std::forward. Tham chiếu chuyển tiếp có thể liên kết với cả giá trị trái và giá trị phải, còn std::forward chuyển đổi đối số có điều kiện về kiểu ban đầu của nó:
#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); // Gọi process(int&), giữ nguyên lvalue
wrapper(20); // Gọi process(int&&), giữ nguyên rvalue
}Nếu không có std::forward, các tham số có tên luôn là giá trị lvalue (lvalue) bên trong hàm (function), ngay cả khi bạn truyền vào một giá trị rvalue (rvalue). Điều này sẽ khiến ngữ nghĩa di chuyển không hoạt động chính xác khi chuyển tiếp đối số đến hàm khởi tạo hoặc các hàm khác.
Chuyển tiếp hoàn hảo là yếu tố thiết yếu khi viết các hàm factory, lớp bao bọc và mọi đoạn code tổng quát cần tạo đối tượng hoặc gọi hàm mà vẫn giữ nguyên ý định của bên gọi về việc di chuyển hay sao chép.
Thử thách
DễHãy tạo một hàm nhà máy minh họa kỹ thuật chuyển tiếp hoàn hảo bằng cách khởi tạo các đối tượng trong khi giữ nguyên loại giá trị của các đối số được truyền vào. Bạn sẽ tạo một hàm tổng quát make_object để chuyển tiếp các đối số đến hàm khởi tạo của một lớp, đảm bảo các giá trị trái được sao chép và các giá trị phải được di chuyển.
Bạn sẽ sắp xếp code trong ba tệp:
Widget.h: Định nghĩa lớpWidgetđể theo dõi cách lớp nhận dữ liệu.Widgetcần lưu trữ tên kiểustd::stringvà giá trị kiểuint. Hãy cung cấp hai hàm khởi tạo:- Một hàm nhận
const std::string&(tham chiếu giá trị trái) vàint: in raWidget constructed (copy): [name] - Một hàm nhận
std::string&&(tham chiếu giá trị phải) vàint: in raWidget constructed (move): [name]và di chuyển chuỗi
Đồng thời thêm phương thức
display()để in ra[name]: [value].- Một hàm nhận
Factory.h: Tạo hàm nhà máy sử dụng kỹ thuật chuyển tiếp hoàn hảo.Viết một hàm mẫu
make_objectnhận tham chiếu chuyển tiếp cho tên và mộtintthông thường cho giá trị. Dùngstd::forwardđể truyền đối số tên đến hàm khởi tạo củaWidgettrong khi vẫn giữ nguyên loại giá trị ban đầu.Hàm cần trả về đối tượng
Widgetđã khởi tạo theo giá trị.main.cpp: Đọc hai đầu vào:- Một tên (chuỗi)
- Một giá trị (số nguyên)
Minh họa kỹ thuật chuyển tiếp hoàn hảo bằng cách tạo hai đối tượng widget:
- Trước tiên, lưu tên vào một biến cục bộ và truyền tên đó đến
make_objectdưới dạng giá trị trái - Tiếp theo, truyền trực tiếp một chuỗi tạm thời (tên đó nối với
"_temp") đếnmake_objectdưới dạng giá trị phải
Sau khi tạo mỗi đối tượng widget, hãy gọi phương thức
display()của đối tượng đó.
Ví dụ, với đầu vào Gadget và 42:
Widget constructed (copy): Gadget
Gadget: 42
Widget constructed (move): Gadget_temp
Gadget_temp: 42Với đầu vào Device và 100:
Widget constructed (copy): Device
Device: 100
Widget constructed (move): Device_temp
Device_temp: 100Điểm mấu chốt là hàm make_object không biết mình nhận giá trị trái hay giá trị phải, nhưng nhờ dùng std::forward, hàm giữ nguyên thông tin đó khi gọi hàm khởi tạo của Widget. Nhờ vậy, quá trình phân giải hàm nạp chồng của hàm khởi tạo sẽ chọn phiên bản phù hợp, cho phép di chuyển hiệu quả các giá trị tạm thời trong khi vẫn sao chép an toàn các biến có tên.
Tự làm thử
#include <iostream>
#include <string>
#include "Widget.h"
#include "Factory.h"
using namespace std;
int main() {
// Đọc đầu vào
string name;
int value;
cin >> name >> value;
// TODO: Tạo widget đầu tiên bằng cách truyền name dưới dạng lvalue
// Lưu name vào một biến cục bộ rồi truyền biến đó cho make_object
// TODO: Gọi display() trên widget đầu tiên
// TODO: Tạo widget thứ hai bằng cách truyền một chuỗi tạm thời dưới dạng rvalue
// Truyền trực tiếp (name + "_temp") cho make_object
// TODO: Gọi display() trên widget thứ hai
return 0;
}
Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.
Tất cả bài học trong Lập trình hướng đối tượng
1Nền tảng về OOP
Tệp bên ngoàiBiên dịch và tạo bản dựng C++Tệp header và tệp nguồnNamespace và phạm viGiới thiệu về OOP trong C++Lớp và đối tượngCon trỏ thisPhương thức (hàm thành viên)Thuộc tính (dữ liệu thành viên)Cơ bản về hàm khởi tạo và hàm hủyÔn tập: máy tính đơn giản4Thuộc tính của lớp
Thành viên đối tượng và thành viên tĩnhHàm truy xuất và hàm thiết lậpHàm thành viên hằngTừ khóa mutablePhương thức và biến tĩnhHàm và lớp bạnÔn tập: Trình quản lý tài khoản ngân hàng7Kế thừa
Kế thừa cơ bảnMức truy cập trong kế thừaThứ tự gọi hàm khởi tạo và hàm hủyGhi đè phương thứcHàm ảo và VTableĐa kế thừaKế thừa ảoÔn tập: hệ phân cấp nhân viên2Quản lý bộ nhớ
Bộ nhớ ngăn xếp và heapCon trỏ và tham chiếuBộ nhớ động (new/delete)Con trỏ thông minh trong C++RAII trong C++Ôn tập: Trình quản lý mảng động5Tính đóng gói
Phạm vi truy cập trong C++Tìm hiểu sâu về phạm vi truy cậpẨn thông tinStruct và lớpLớp lồng nhau và lớp bên trongÔn tập: hệ thống hồ sơ sinh viên8Đa hình
Đa hình lúc biên dịch và khi chạyNạp chồng hàmÔn lại hàm ảoHàm ảo thuần túyLớp trừu tượngThiết kế interface trong C++Ép kiểu động và RTTIÔn tập: Máy tính hình học3Hàm khởi tạo và hàm hủy
Hàm khởi tạo mặc địnhHàm khởi tạo có tham sốHàm khởi tạo sao chépHàm khởi tạo di chuyểnDanh sách khởi tạoHàm khởi tạo ủy quyềnTìm hiểu sâu về hàm hủyQuy tắc 3 / 5 / 0Ôn tập: lớp chuỗi6Nạp chồng toán tử
Giới thiệu về nạp chồng toán tửNạp chồng toán tử số họcNạp chồng toán tử so sánhToán tử luồngNạp chồng toán tử gánNạp chồng toán tử [] và ()Toán tử chuyển đổi kiểuÔn tập: lớp ma trận9Mẫu
Mẫu hàmMẫu lớpChuyên biệt hóa mẫuMẫu biến tham sốCơ bản về SFINAE và đặc điểm kiểuÔn tập: vùng chứa tổng quát12Các tính năng hiện đại của C++
Ngữ nghĩa di chuyển và giá trị rvalueChuyển tiếp hoàn hảoTìm hiểu sâu về biểu thức lambdastd::function và std::bindconstexpr và constevalRàng buộc cấu trúcoptional, variant, anyTự luyện tập: Trình biên dịch C++ online