رفتن به محتوا

لامبداها و برنامه‌نویسی تابعی

لامبداهای ++C را فرا بگیرید — لیست‌های گرفتن، لامبداهای mutable، لامبداهای عمومی و استفاده آن‌ها به عنوان محمول‌های الگوریتم. std::function، std::invoke و الگوی IIFE را بیاموزید.

عبارات لامبدا

یک عبارت لامبدا (Lambda Expression) یک شیء تابع بی‌نام (بسته‌بندی یا closure) به صورت درون‌خطی ایجاد می‌کند. لامبداها راه اصلی ارسال رفتار به الگوریتم‌های STL، بازفراخوانی‌ها (callbacks) و مدیریت‌کننده‌های رویداد هستند.

نحو کامل:

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

- گرفتن‌ها (Captures) — متغیرهایی از محدوده بیرونی برای استفاده داخل لامبدا
- پارامترها — مانند پارامترهای تابع معمولی
- mutable — اجازه تغییر متغیرهای گرفته‌شده با مقدار را می‌دهد
- نوع بازگشتی — معمولاً به طور خودکار استنباط می‌شود
- بدنه — بدنه تابع

لامبداها به اشیاء تابع تولیدشده توسط کامپایلر (ساختارهایی با operator()) کامپایل می‌شوند — سربار صفر در مقایسه با فانکتورهای دست‌نوشته دارند.

نحو پایه لامبدا و گرفتن‌ها

لیست گرفتن ویژگی تعیین‌کننده لامبداها است. مشخص می‌کند کدام متغیرهای محدوده بیرونی قابل دسترسی برای لامبدا هستند و چگونه.

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
}

لامبداهای عمومی و قالبی

‎C++‎14 لامبداهای عمومی (Generic Lambdas) را با پارامترهای auto معرفی کرد. ‎C++‎20 این را با لیست‌های پارامتر قالب صریح گسترش داد و قدرت کامل قالب را در لامبداها فراهم کرد.

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 و std::invoke

std::function یک پوشش قابل‌فراخوانی با حذف نوع است که می‌تواند هر قابل‌فراخوانی با امضای مطابق را نگه دارد — لامبداها، اشاره‌گرهای تابع، فانکتورها، اشاره‌گرهای تابع عضو. std::invoke روشی یکنواخت برای فراخوانی هر قابل‌فراخوانی است.

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)
}

الگوی 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; }` چه چیزی را نشان می‌دهد؟