Menu
CoddyTech

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ũ.

challenge icon

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ổng
    • subtract(int a, int b): trả về hiệu (a - b)
    • multiply(int a, int b): trả về tích
    • power(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ớp Calculator, dùng std::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ại
    • calculate(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.

  • main.cpp: Đọc ba đầu vào:
    1. Tên phép toán: add, subtract, multiply hoặc square
    2. Số thứ nhất (số nguyên)
    3. Số thứ hai (số nguyên)

    Tạo một Calculator và cấu hình dựa trên tên phép toán:

    • Với add, subtract và multiply: gán trực tiếp hàm tương ứng cho máy tính
    • Với square: dùng std::bind để tạo một phiên bản chuyên biệt của power, 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]

Ví dụ, với các đầu vào add, 10 và 5:

Result: 15
With bonus: 115

Với các đầu vào multiply, 7 và 3:

Result: 21
With bonus: 110

Với các đầu vào square, 4 và 0:

Result: 16
With bonus: 104

Thử 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;
}
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