Chuyển đến nội dung

Lambdas & Lập trình chức năng

Các lambda C++ chính — danh sách chụp, các lambda có thể thay đổi, các lambda chung và việc sử dụng chúng làm vị từ thuật toán. Tìm hiểu std::function, std::invoke và mẫu IIFE.

Lambda Expressions

biểu thức lambda tạo một đối tượng hàm ẩn danh (đóng) nội tuyến. Lambda là cách chính để chuyển hành vi sang thuật toán STL, lệnh gọi lại và trình xử lý sự kiện.

Cú pháp đầy đủ là:

[captures](parameters) mutable -> return_type { body }

- Captures — variables from the enclosing scope to use inside the lambda
- Parameters — like regular function parameters
- mutable — allows modifying captured-by-value variables
- Return type — usually deduced automatically
- Body — the function body

Lambdas compile down to compiler-generated function objects (structs with operator()) — chúng không tốn chi phí so với các functor viết tay.

Cú pháp và ảnh chụp Lambda cơ bản

Danh sách chụp là tính năng xác định của lambdas. Nó xác định những biến kèm theo mà lambda có thể truy cập và cách truy cập.

lambda_captures.cpp
#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> nums = {1, 5, 3, 8, 2, 9, 4, 7, 6};

    // Simplest lambda — no captures
    std::sort(nums.begin(), nums.end(),
              [](int a, int b) { return a > b; });  // descending

    // Capture by value [=] — copies variables
    int threshold = 5;
    auto above = std::count_if(nums.begin(), nums.end(),
                               [threshold](int x) { return x > threshold; });
    std::cout << "Above " << threshold << ": " << above << '\n';

    // Capture by reference [&] — references variables
    int sum = 0;
    std::for_each(nums.begin(), nums.end(),
                  [&sum](int x) { sum += x; });
    std::cout << "Sum: " << sum << '\n';

    // Mixed captures
    int min_val = 3, max_val = 7;
    auto in_range = std::count_if(nums.begin(), nums.end(),
        [min_val, &max_val](int x) { return x >= min_val && x <= max_val; });
    // min_val captured by value, max_val captured by reference

    // Capture all by value [=] or all by reference [&]
    auto print_all = [&]() {  // captures everything by reference
        for (int n : nums) std::cout << n << ' ';
        std::cout << '\n';
    };
    print_all();

    // Init capture (C++14) — create new variables in the capture
    auto counter = [count = 0]() mutable { return ++count; };
    std::cout << counter() << '\n';  // 1
    std::cout << counter() << '\n';  // 2
    std::cout << counter() << '\n';  // 3
}

Generic and Template Lambdas

C++14 đã giới thiệu lambda chung với thông sốauto. C++20 đã mở rộng tính năng này bằng danh sách tham số mẫu rõ ràng, cho phép cung cấp toàn bộ sức mạnh cho mẫu trong lambdas.

generic_lambdas.cpp
#include <algorithm>
#include <vector>
#include <string>
#include <iostream>
#include <type_traits>

int main() {
    // C++14: generic lambda — auto parameters
    auto print = [](const auto& x) {
        std::cout << x << '\n';
    };
    print(42);           // int
    print(3.14);         // double
    print("hello");      // const char*

    // C++14: generic lambda for comparisons
    auto max_of = [](const auto& a, const auto& b) {
        return (a > b) ? a : b;
    };
    std::cout << max_of(3, 7) << '\n';             // 7
    std::cout << max_of("abc", "xyz") << '\n';     // xyz (lexicographic)

    // C++20: template lambda — explicit template parameters
    auto add = []<typename T>(T a, T b) -> T {
        return a + b;
    };
    std::cout << add(1, 2) << '\n';                // 3
    std::cout << add(1.5, 2.5) << '\n';            // 4.0

    // C++20: constrained template lambda with concepts
    auto integral_only = []<std::integral T>(T x) {
        return x * 2;
    };
    std::cout << integral_only(5) << '\n';         // 10
    // integral_only(3.14);  // ERROR: double doesn't satisfy std::integral

    // Using generic lambdas with algorithms
    std::vector<std::string> words = {"banana", "apple", "cherry"};
    std::sort(words.begin(), words.end(),
              [](const auto& a, const auto& b) {
                  return a.size() < b.size();  // sort by length
              });
    for (const auto& w : words) std::cout << w << ' ';  // apple banana cherry
    std::cout << '\n';
}

std::function và std::invoke

std::functionlà một trình bao bọc có thể gọi được đã xóa kiểu, có thể chứa bất kỳ lệnh gọi nào có chữ ký phù hợp - lambdas, con trỏ hàm, hàm functor, con trỏ hàm thành viên.std::invokelà một cách thống nhất để gọi bất kỳ lệnh gọi nào.

std_function.cpp
#include <functional>
#include <iostream>
#include <string>
#include <vector>

// Regular function
int add(int a, int b) { return a + b; }

struct Multiplier {
    int factor;
    int operator()(int x) const { return x * factor; }
    int multiply(int x) const { return x * factor; }
};

int main() {
    // std::function can hold different callable types
    std::function<int(int, int)> op;

    op = add;                                        // function pointer
    std::cout << op(3, 4) << '\n';                   // 7

    op = [](int a, int b) { return a * b; };         // lambda
    std::cout << op(3, 4) << '\n';                   // 12

    // Store callables in a collection
    std::vector<std::function<int(int)>> transforms;
    transforms.push_back([](int x) { return x + 1; });
    transforms.push_back([](int x) { return x * 2; });
    transforms.push_back(Multiplier{10});            // functor

    int val = 5;
    for (auto& fn : transforms) {
        val = fn(val);
    }
    std::cout << val << '\n';  // ((5+1)*2)*10 = 120

    // std::invoke — uniform call syntax for any callable
    Multiplier m{3};
    std::cout << std::invoke(add, 10, 20)       << '\n';  // 30
    std::cout << std::invoke(m, 7)              << '\n';  // 21
    std::cout << std::invoke(&Multiplier::multiply, m, 7) << '\n';  // 21
    std::cout << std::invoke(&Multiplier::factor, m)      << '\n';  // 3 (data member)
}

Mẫu IIFE

IIFE (Biểu thức hàm được gọi ngay lập tức) là một mẫu được mượn từ JavaScript. Bạn xác định lambda và gọi nó ngay lập tức. Điều này rất hữu ích cho việc khởi tạo phức tạp các biếnconst:

const auto config = [&]() {
    Config c;
    c.host = read_env("HOST");
    c.port = parse_int(read_env("PORT"));
    if (c.port <= 0) c.port = 8080;
    c.debug = is_debug_build();
    return c;
}();  // <-- note the () — invoked immediately

Without IIFE, you would either need a non-const variable (losing immutability guarantees) or a separate factory function. IIFE keeps the initialization logic local and the result const.

Lỗi thường gặp

Việc chụp theo tham chiếu rất nguy hiểm khi lambda tồn tại lâu hơn biến bị bắt:

std::function<int()> make_counter() {
    int count = 0;
    return [&count]() { return ++count; };  // BUG: count is destroyed!
}

The returned lambda holds a reference to a local variable that no longer exists. This is undefined behavior. Fix it by capturing by value (with mutable if needed):

std::function<int()> make_counter() {
    int count = 0;
    return [count]() mutable { return ++count; };  // OK: owns a copy
}

Nguyên tắc nhỏ: thu thập bằng tham chiếu đối với lambda tồn tại trong thời gian ngắn (vị ngữ thuật toán), thu thập theo giá trị đối với lambda tồn tại lâu dài (lệnh gọi lại, được lưu trữ trong cấu trúc dữ liệu).

Thực hành tốt

std::functioncó chi phí chung: nó phân bổ bộ nhớ (đối với các lần đóng lớn), sử dụng công văn ảo và ngăn nội tuyến. Đối với các vị từ thuật toán và các trường hợp khác trong đó hàm có thể gọi được sử dụng ngay lập tức, hãy chuyển trực tiếp lambda — trình biên dịch có thể nội tuyến nó hoàn toàn.

Chỉ sử dụngstd::functionkhi bạn cần:
- Lưu trữ các giá trị có thể gọi để sử dụng sau
- Gõ-xóa các loại có thể gọi khác nhau vào một vùng chứa thống nhất
- Chuyển các lệnh gọi được qua ranh giới ABI

Trong các mẫu, hãy sử dụngautohoặc các khái niệm để chấp nhận các lệnh gọi được mà không cần xóa kiểu:

template<std::invocable<int> F>
void apply(F&& fn, int value) {
    fn(value);  // no std::function overhead
}
Điểm chính
  • Lambda là các đối tượng hàm ẩn danh không tốn chi phí — sử dụng chúng một cách thoải mái với thuật toán STL
  • Ghi lại theo giá trị[x]để đảm bảo an toàn với lambdas tồn tại lâu dài; chụp bằng tham chiếu[&x]cho những cái có thời gian tồn tại ngắn
  • Lambda chung C++14 (thông sốauto) và lambda mẫu C++20 cung cấp toàn bộ sức mạnh lập trình chung
  • Kiểustd::functionxóa các lệnh gọi được nhưng có chi phí chung - ưu tiênautohoặc mẫu khi có thể
  • Mẫu IIFE[&]() { ... }()cho phép khởi tạo các biếnconstphức tạp
  • Bản chụp ban đầu ([x = expr]) cho phép tạo các biến mới trong phần đóng (C++14)

Câu đố — Kiểm tra kiến thức

(15 XP)

1. Từ khóa`mutable`làm gì trên lambda?

2. Nguy hiểm của việc nắm bắt một biến cục bộ bằng cách tham chiếu trong lambda được trả về từ một hàm là gì?

3. What does`[count = 0]() mutable { return ++count; }`demonstrate?