std::function và std::bind
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 86 trên 104.
Mặc dù lambda rất mạnh mẽ, đôi khi bạn cần lưu trữ các đối tượng có thể gọi thuộc nhiều kiểu khác nhau theo một cách thống nhất, hoặc điều chỉnh các hàm hiện có để khớp với chữ ký hàm bắt buộc. std::function và std::bind trong tiêu đề <functional> giúp giải quyết những vấn đề này.
std::function là một lớp bao bọc xóa kiểu (type-erased wrapper), có thể chứa bất kỳ đối tượng gọi được nào khớp với một chữ ký hàm cụ thể, chẳng hạn như hàm, lambda hoặc đối tượng hàm:
#include <iostream>
#include <functional>
int add(int a, int b) { return a + b; }
int main() {
std::function<int(int, int)> operation;
operation = add; // Hàm thông thường
std::cout << operation(3, 4) << "\n"; // 7
operation = [](int a, int b) { return a * b; }; // Lambda
std::cout << operation(3, 4) << "\n"; // 12
}std::bind tạo một hàm có thể gọi mới bằng cách cố định một số đối số của hàm hiện có. Dùng std::placeholders::_1, _2, v.v. để đánh dấu các đối số vẫn còn thay đổi:
#include <iostream>
#include <functional>
void greet(const std::string& greeting, const std::string& name) {
std::cout << greeting << ", " << name << "!\n";
}
int main() {
using namespace std::placeholders;
auto sayHello = std::bind(greet, "Hello", _1);
sayHello("Alice"); // Hello, Alice!
auto swapped = std::bind(greet, _2, _1);
swapped("Bob", "Hi"); // Hi, Bob!
}Các công cụ này đặc biệt hữu ích trong lập trình hướng đối tượng (OOP) để lưu callback làm thành viên lớp hoặc triển khai mẫu Strategy. Tuy nhiên, C++ hiện đại thường ưu tiên biểu thức lambda hơn std::bind vì dễ đọc và hiệu quả hơn, chủ yếu dùng std::bind khi bạn cần đổi thứ tự đối số hoặc làm việc với code cũ.
Thử thách
DễHãy xây dựng một máy tính có thể tùy chỉnh để minh họa sức mạnh của std::function và std::bind. Bạn sẽ tạo một hệ thống có thể lưu trữ, thay đổi và tùy chỉnh các phép toán khi chương trình đang chạy, qua đó cho thấy những công cụ này giúp quản lý callback linh hoạt như thế nào.
Bạn sẽ sắp xếp code trong ba tệp:
MathOperations.h: Định nghĩa một tập hợp các hàm toán học độc lập, dùng làm thư viện phép toán.Tạo các hàm sau:
add(int a, int b): trả về tổngsubtract(int a, int b): trả về hiệu (a - b)multiply(int a, int b): trả về tíchpower(int base, int exponent, int multiplier): trả vềmultiplier * (base ^ exponent). Dùng một vòng lặp đơn giản để tính lũy thừa (giả sử số mũ không âm).
Calculator.h: Định nghĩa lớpCalculator, dùngstd::functionđể lưu trữ và thực thi các phép toán một cách linh hoạt.Calculator cần có:
- Một thành viên riêng tư
std::function<int(int, int)>để lưu phép toán hai ngôi hiện tại setOperation(std::function<int(int, int)> op): đặt phép toán hiện tạicalculate(int a, int b): thực thi phép toán đã lưu và trả về kết quả
Thêm các header cần thiết (
<functional>) và nhớ thêm header MathOperations của bạn.- Một thành viên riêng tư
main.cpp: Đọc ba đầu vào:- Tên phép toán:
add,subtract,multiplyhoặcsquare - Số thứ nhất (số nguyên)
- Số thứ hai (số nguyên)
Tạo một
Calculatorvà cấu hình dựa trên tên phép toán:- Với
add,subtractvàmultiply: gán trực tiếp hàm tương ứng cho máy tính - Với
square: dùngstd::bindđể tạo một phiên bản chuyên biệt củapower, luôn dùng số mũ 2 và hệ số nhân 1. Hàm đã liên kết cần nhận hai đối số, trong đó chỉ đối số thứ nhất được dùng làm cơ số (có thể bỏ qua đối số thứ hai bằng một placeholder).
Sau khi đặt phép toán, gọi
calculate()với hai số của bạn và in ra:Result: [value]Sau đó, minh họa việc thay đổi phép toán bằng cách gán cho máy tính một lambda trả về
a + b + 100, chạy với cùng các đầu vào và in ra:With bonus: [value]- Tên phép toán:
Ví dụ, với các đầu vào add, 10 và 5:
Result: 15
With bonus: 115Với các đầu vào multiply, 7 và 3:
Result: 21
With bonus: 110Với các đầu vào square, 4 và 0:
Result: 16
With bonus: 104Thử thách này cho thấy std::function cung cấp một cách thống nhất để lưu trữ nhiều kiểu đối tượng có thể gọi khác nhau (hàm thông thường, hàm đã liên kết và lambda), còn std::bind cho phép bạn điều chỉnh các hàm có sẵn bằng cách cố định một số đối số của chúng.
Tự làm thử
#include <iostream>
#include <string>
#include <functional>
#include "Calculator.h"
using namespace std;
int main() {
// Đọc đầu vào
string operation;
int num1, num2;
cin >> operation >> num1 >> num2;
// Tạo một instance của Calculator
Calculator calc;
// TODO: Cấu hình calculator theo tên phép tính
// Với "add", "subtract", "multiply": gán trực tiếp hàm (function) tương ứng
// Với "square": dùng std::bind để tạo phiên bản chuyên biệt của power
// luôn dùng số mũ 2 và hệ số nhân 1
// Gợi ý: Dùng std::placeholders::_1 cho đối số (argument) cơ số
if (operation == "add") {
// Viết code tại đây
} else if (operation == "subtract") {
// Viết code tại đây
} else if (operation == "multiply") {
// Viết code tại đây
} else if (operation == "square") {
// Viết code tại đây: dùng std::bind với hàm power
}
// TODO: Gọi calculate() và in ra kết quả
// Định dạng: "Result: [value]"
// TODO: Minh họa cách thay đổi phép tính
// Gán cho calculator một hàm lambda trả về a + b + 100
// Gọi lại calculate() và in ra kết quả
// Định dạng: "With bonus: [value]"
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