لامبداها و برنامهنویسی تابعی
لامبداهای ++C را فرا بگیرید — لیستهای گرفتن، لامبداهای mutable، لامبداهای عمومی و استفاده آنها به عنوان محمولهای الگوریتم. std::function، std::invoke و الگوی IIFE را بیاموزید.
عبارات لامبدا
یک عبارت لامبدا (Lambda Expression) یک شیء تابع بینام (بستهبندی یا closure) به صورت درونخطی ایجاد میکند. لامبداها راه اصلی ارسال رفتار به الگوریتمهای STL، بازفراخوانیها (callbacks) و مدیریتکنندههای رویداد هستند.
نحو کامل:
[captures](parameters) mutable -> return_type { body }- گرفتنها (Captures) — متغیرهایی از محدوده بیرونی برای استفاده داخل لامبدا
- پارامترها — مانند پارامترهای تابع معمولی
- mutable — اجازه تغییر متغیرهای گرفتهشده با مقدار را میدهد
- نوع بازگشتی — معمولاً به طور خودکار استنباط میشود
- بدنه — بدنه تابع
لامبداها به اشیاء تابع تولیدشده توسط کامپایلر (ساختارهایی با operator()) کامپایل میشوند — سربار صفر در مقایسه با فانکتورهای دستنوشته دارند.
نحو پایه لامبدا و گرفتنها
لیست گرفتن ویژگی تعیینکننده لامبداها است. مشخص میکند کدام متغیرهای محدوده بیرونی قابل دسترسی برای لامبدا هستند و چگونه.
#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
}لامبداهای عمومی و قالبی
C++14 لامبداهای عمومی (Generic Lambdas) را با پارامترهای auto معرفی کرد. C++20 این را با لیستهای پارامتر قالب صریح گسترش داد و قدرت کامل قالب را در لامبداها فراهم کرد.
#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 و std::invoke
std::function یک پوشش قابلفراخوانی با حذف نوع است که میتواند هر قابلفراخوانی با امضای مطابق را نگه دارد — لامبداها، اشارهگرهای تابع، فانکتورها، اشارهگرهای تابع عضو. std::invoke روشی یکنواخت برای فراخوانی هر قابلفراخوانی است.
#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)
}الگوی IIFE
IIFE (عبارت تابع فوراً فراخوانیشده — Immediately Invoked Function Expression) الگویی است که از جاوااسکریپت وام گرفته شده. یک لامبدا تعریف میکنید و فوراً آن را فراخوانی میکنید. این برای مقداردهی اولیه پیچیده متغیرهای const مفید است:
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بدون IIFE، یا به یک متغیر غیر const نیاز دارید (از دست دادن تضمینهای تغییرناپذیری) یا یک تابع کارخانه جداگانه. IIFE منطق مقداردهی اولیه را محلی نگه میدارد و نتیجه const میماند.
گرفتن با ارجاع وقتی لامبدا بیشتر از متغیر گرفتهشده عمر کند خطرناک است:
std::function<int()> make_counter() {
int count = 0;
return [&count]() { return ++count; }; // BUG: count is destroyed!
}لامبدای بازگشتی ارجاعی به یک متغیر محلی دارد که دیگر وجود ندارد. این رفتار نامشخص است. با گرفتن با مقدار (با mutable در صورت نیاز) آن را اصلاح کنید:
std::function<int()> make_counter() {
int count = 0;
return [count]() mutable { return ++count; }; // OK: owns a copy
}قاعده سرانگشتی: برای لامبداهای کوتاهعمر (محمولهای الگوریتم) با ارجاع بگیرید، برای لامبداهای بلندعمر (بازفراخوانیها، ذخیره در ساختارهای داده) با مقدار بگیرید.
std::function سربار دارد: حافظه تخصیص میدهد (برای بستهبندیهای بزرگ)، از ارسال مجازی استفاده میکند و از درونخطیسازی (inlining) جلوگیری میکند. برای محمولهای الگوریتم و موارد دیگری که قابلفراخوانی فوراً استفاده میشود، لامبدا را مستقیماً ارسال کنید — کامپایلر میتواند آن را کاملاً درونخطی کند.
فقط وقتی نیاز دارید از std::function استفاده کنید:
- قابلفراخوانیها را برای استفاده بعدی ذخیره کنید
- انواع مختلف قابلفراخوانی را در یک کانتینر یکنواخت حذف نوع کنید
- قابلفراخوانیها را از مرزهای ABI عبور دهید
در قالبها، از auto یا مفاهیم برای پذیرش قابلفراخوانیها بدون حذف نوع استفاده کنید:
template<std::invocable<int> F>
void apply(F&& fn, int value) {
fn(value); // no std::function overhead
}- لامبداها اشیاء تابع بینام با سربار صفر هستند — آزادانه با الگوریتمهای STL استفاده کنید
- برای ایمنی با لامبداهای بلندعمر با مقدار
[x]بگیرید؛ برای لامبداهای کوتاهعمر با ارجاع[&x]بگیرید - لامبداهای عمومی C++14 (پارامترهای
auto) و لامبداهای قالبی C++20 قدرت کامل برنامهنویسی عمومی را میدهند std::functionقابلفراخوانیها را حذف نوع میکند اما سربار دارد — وقتی ممکن استautoیا قالبها را ترجیح دهید- الگوی IIFE یعنی
[&]() { ... }()مقداردهی اولیه پیچیده متغیرهایconstرا ممکن میسازد - گرفتنهای مقداردهی اولیه (
[x = expr]) اجازه ایجاد متغیرهای جدید در بستهبندی را میدهند (C++14)
آزمون — دانش خود را بسنجید
(15 امتیاز)1. کلمه کلیدی `mutable` روی لامبدا چه کاری انجام میدهد؟
2. خطر گرفتن یک متغیر محلی با ارجاع در لامبدایی که از تابع بازگردانده میشود چیست؟
3. `[count = 0]() mutable { return ++count; }` چه چیزی را نشان میدهد؟