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.
#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.
#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.
#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 immediatelyWithout 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.
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).
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
}- 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ểu
std::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?