Menu
CoddyTech

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.

challenge icon

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ớp Widget để theo dõi cách lớp nhận dữ liệu.

    Widget cần lưu trữ tên kiểu std::string và giá trị kiểu int. 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 ra Widget constructed (copy): [name]
    • Một hàm nhận std::string&& (tham chiếu giá trị phải) và int: in ra Widget constructed (move): [name] và di chuyển chuỗi

    Đồng thời thêm phương thức display() để in ra [name]: [value].

  • 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_object nhận tham chiếu chuyển tiếp cho tên và một int thông thường cho giá trị. Dùng std::forward để truyền đối số tên đến hàm khởi tạo của Widget trong 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:
    1. Một tên (chuỗi)
    2. 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:

    1. Trước tiên, lưu tên vào một biến cục bộ và truyền tên đó đến make_object dưới dạng giá trị trái
    2. Tiếp theo, truyền trực tiếp một chuỗi tạm thời (tên đó nối với "_temp") đến make_object dướ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: 42

Vớ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;
}
quiz iconTự kiểm tra

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

Tự luyện tập: Trình biên dịch C++ online