সরাসরি পর্বে যান
পর্ব ৩৪ · ৫৫-এর মধ্যে · মডিউল ৫

lambda ও function object — দরকারের জায়গাতেই ছোট ফাংশন

Lambdas and Function Objects — Small Functions Right Where You Need Them

Taught in Bangla, this lesson explains lambdas as small inline functions: their four-part syntax, the closure class the compiler generates, capture by value and by reference, init-capture and mutable, dangling captures, generic lambdas with STL algorithms, and storing callables with auto, std::function or function pointers.

এই পর্বে যা শিখবেন

  • lambda কোন সমস্যার সমাধান করে, আর এর গঠনের প্রতিটি অংশ কী কাজ করে
  • compiler একটি lambda থেকে কোন closure class বানায় — পর্ব ২৩-এর functor-এর সাথে মিলিয়ে
  • capture-এর সব রূপ: [x], [&x], [=], [&], init-capture, this আর [*this]; সাথে mutable
  • reference capture কখন dangling হয়, আর callback বা সংরক্ষিত lambda-তে কীভাবে তা এড়াবেন
  • generic ও template lambda, STL algorithm-এ lambda, std::function বনাম auto বনাম function pointer, আর AI-এর লেখা lambda যাচাই

১ · সমস্যাটা আগে — এক লাইনের নিয়মের জন্য পুরো একটি ফাংশন?

পর্ব ৩৩-এ আপনি দেখেছেন, STL algorithm-গুলো কাজের “কাঠামো” নিজে দেয়, আর আপনাকে শুধু “নিয়মটি” দিতে হয়। std::ranges::sort জানে কীভাবে সাজাতে হয়, কিন্তু জানে না আপনি দাম দিয়ে সাজাবেন, নাকি নাম দিয়ে। count_if জানে কীভাবে গুনতে হয়, কিন্তু জানে না কোন শিক্ষার্থীকে “পাস” ধরবেন। এই নিয়ম পাঠানোর জন্য দরকার এমন কিছু, যাকে ফাংশনের মতো ডাকা যায় — একটি predicate (হ্যাঁ/না উত্তর দেয় এমন নিয়ম) বা একটি comparator (দুটির মধ্যে কে আগে, তা বলে এমন নিয়ম)।

C++11-এর আগে এর দুটি উপায় ছিল, আর দুটিই অস্বস্তিকর। প্রথম উপায়: ফাইলের কোথাও একটি সাধারণ ফাংশন লেখা, যেমন bool cheaper(const Product&, const Product&). কিন্তু যে নিয়ম মাত্র একবার, একটি জায়গায় লাগে, তার জন্য আলাদা নাম আর আলাদা জায়গা — পড়ার সময় পাঠককে লাফ দিয়ে অন্য জায়গায় গিয়ে দেখতে হয় নিয়মটা কী। আর পাসের নম্বর যদি চলার সময় ব্যবহারকারী ঠিক করেন (ধরুন ৪০ বা ৫০), সাধারণ ফাংশন সেই মান নিজের সাথে রাখতে পারে না — global ভ্যারিয়েবল ছাড়া।

দ্বিতীয় উপায় পর্ব ২৩-এর function object, অর্থাৎ functor: একটি class, যার operator() আছে, আর ভেতরে পাসের নম্বরটি একটি data member হিসেবে থাকে। এটি কাজ করে, আর মান সঙ্গে রাখতেও পারে। কিন্তু এক লাইনের নিয়মের জন্য লিখতে হয় পুরো একটি class:

// পুরোনো পথ: functor (পর্ব ২৩) — কাজ করে, কিন্তু অনেক লেখা
struct PassesAt {
    int limit;                                       // সঙ্গে রাখা তথ্য
    bool operator()(int marks) const { return marks >= limit; }
};
auto n = std::ranges::count_if(marks, PassesAt{40});   // ব্যবহার অনেক দূরে, অন্য জায়গায়

// আধুনিক পথ: lambda — নিয়মটি ঠিক যেখানে দরকার
int limit = 40;
auto m = std::ranges::count_if(marks, [limit](int x) { return x >= limit; });

দ্বিতীয় লাইনটি পড়ুন: “marks-এর মধ্যে কয়টি এমন, যার মান limit বা তার বেশি।” নিয়মটি ঠিক সেখানেই লেখা, যেখানে ব্যবহার হচ্ছে, আর limit-এর মান সঙ্গে নিয়ে গেছে। এটাই lambda — C++11 থেকে ভাষার অংশ।

অফিসে একজন পিয়নকে একটি খাম দিয়ে পাঠানোর কথা ভাবুন। খামের ভেতরে একটি চিরকুট: “যার বিল ৫০০ টাকার বেশি, শুধু তাকে এটি দেবেন।” পিয়নকে নাম দিয়ে আলাদা করে প্রশিক্ষণ দিতে হয়নি, কোনো স্থায়ী নিয়মের খাতাও লাগেনি — কাজটা এই একবারের, তাই নির্দেশও এখানেই লেখা। আর চিরকুটে লেখা “৫০০” সংখ্যাটি খামের সাথেই চলে যায়, অফিসের মূল খাতায় পরে কেউ সংখ্যা বদলালেও চিরকুটের লেখা বদলায় না। lambda হলো সেই খাম: ছোট একটি নির্দেশ, যা দরকারের জায়গাতেই লেখা, আর সাথে নেওয়া কিছু তথ্য। সাথে নেওয়া তথ্যকে বলে capture — আর চিরকুটে সংখ্যা লিখে দেবেন, নাকি লিখবেন “মূল খাতায় যা আছে দেখে নেবেন” — এই সিদ্ধান্তই এই পর্বের সবচেয়ে গুরুত্বপূর্ণ বিষয়।

মূল ধারণা

lambda নতুন কোনো ক্ষমতা আনেনি — functor দিয়ে যা যা করা যেত, lambda দিয়েও ঠিক তাই করা যায়। এর অবদান হলো সংক্ষেপ আর স্থানীয়তা: যে নিয়ম যেখানে লাগে, সেখানেই এক লাইনে লেখা যায়, আর compiler বাকি class-টি নিজে বানিয়ে দেয়। ছোট, একবারের নিয়ম হলে lambda; বড়, বারবার লাগে বা নাম দিলে বোঝা সহজ হয় — তাহলে নাম দেওয়া ফাংশন বা class.

২ · lambda-র গঠন — চারটি অংশ

একটি পূর্ণ lambda দেখতে এমন: [capture](params) -> ret { body }. প্রতিটি অংশ আলাদা করে দেখুন:

অংশউদাহরণকাজ
capture list[limit]বাইরের কোন ভ্যারিয়েবল সঙ্গে যাবে, আর কীভাবে (কপি না reference). খালি [] মানে কিছুই নয়। এই বন্ধনীই বলে দেয় “এখানে একটি lambda শুরু”।
প্যারামিটার(int x)সাধারণ ফাংশনের মতোই — ডাকার সময় যা দেওয়া হবে। কোনো প্যারামিটার না থাকলে () বাদও দেওয়া যায়।
ফেরত-টাইপ-> boolঐচ্ছিক। না লিখলে compiler return দেখে নিজে টাইপ ঠিক করে (return type deduction). লেখার রীতিটি পর্ব ৯-এর trailing return type-এর মতো।
body{ return x >= limit; }সাধারণ ফাংশনের body — যত লাইন দরকার। কিন্তু lambda যত বড়, নাম দেওয়া ফাংশন বানানোর যুক্তি তত শক্ত।

সবচেয়ে ছোট বৈধ lambda হলো []{} — কিছু capture করে না, কিছু নেয় না, কিছু করে না। কয়েকটি সাধারণ রূপ:

auto hello  = [] { std::println("hello"); };          // প্যারামিটার নেই, () বাদ
auto square = [](int x) { return x * x; };            // ফেরত-টাইপ: int (নিজে বুঝে নেয়)
auto ratio  = [](int a, int b) -> double {            // ফেরত-টাইপ নিজে লেখা
    if (b == 0) return 0;                             // 0 (int) কে double বানাবে
    return static_cast<double>(a) / b;
};
hello();                 // সাধারণ ফাংশনের মতো ডাক
int s = square(7);       // 49
[](int x) { std::println("{}", x); }(5);   // লিখেই সঙ্গে সঙ্গে ডাকা — পরে কাজে লাগবে

ratio-তে ফেরত-টাইপ কেন লিখতে হলো, লক্ষ করুন। দুটি return-এর একটি দেয় int (0), অন্যটি double. ফেরত-টাইপ না লিখলে compiler দুটি আলাদা টাইপ দেখে error দিত — সব return-কে একই টাইপ দিতে হয়। -> double লিখে দিলে সমস্যা মেটে। বেশিরভাগ ছোট lambda-তে অবশ্য এটা লাগে না।

আরেকটি বিষয়: lambda-কে প্রায় সবসময় auto ভ্যারিয়েবলে রাখা হয়। কেন? কারণ প্রতিটি lambda-র টাইপ আলাদা আর নামহীন — আপনি টাইপটির নাম লিখতেই পারবেন না। পরের অংশে দেখবেন, কেন এমন।

৩ · ভেতরে কী ঘটে — compiler বানায় একটি closure class

lambda কোনো জাদু নয়। compiler প্রতিটি lambda expression দেখে একটি নতুন, নাম-ছাড়া class বানায় — যাকে বলে closure type. আর lambda expression-টি যেখানে লেখা, চলার সময় সেখানে সেই class-এর একটি object তৈরি হয় — যার নাম closure. অর্থাৎ lambda হলো আপনার লেখা কোড, আর closure হলো চলার সময়ের object — ঠিক যেমন class আর object-এর সম্পর্ক। পর্ব ২৩-এর functor-টি compiler আপনার হয়ে লিখে দিচ্ছে। ধাপে ধাপে দেখুন:

আপনি যা লিখলেনএক লাইনের lambda
int limit = 40;
auto pass = [limit](int m) { return m >= limit; };
bool ok = pass(45);
ধাপ ১: নতুন classনাম compiler-এর বানানো
class __lambda_7 {          // আসল নাম লুকানো; প্রতিটি lambda-র আলাদা
    ...
};
// দুটি হুবহু একই lambda লিখলেও দুটি আলাদা class হয়
ধাপ ২: capture → data member[limit] = একটি কপি
class __lambda_7 {
    int limit;              // [limit] থেকে — নিজের একটি কপি
public:
    __lambda_7(int l) : limit(l) {}   // তৈরির সময় কপি নেয়
};
// [&limit] হলে এখানে থাকত: int& limit;
ধাপ ৩: body → operator()ডিফল্টভাবে const
class __lambda_7 {
    int limit;
public:
    __lambda_7(int l) : limit(l) {}
    bool operator()(int m) const { return m >= limit; }
};          //                ^^^^^ তাই body-তে limit বদলানো যায় না
ধাপ ৪: ব্যবহারobject তৈরি আর ডাক
__lambda_7 pass{limit};          // lambda expression = closure object তৈরি
bool ok = pass.operator()(45);   // pass(45) আসলে এই ডাক
// ফল: 45 >= 40, তাই true
  1. আপনার কোড। একটি lambda, যা limit capture করে আর একটি নম্বর নেয়। দেখতে ফাংশনের মতো, কিন্তু এটি আসলে একটি expression — যার মান একটি object. সেই object-টিকে pass নামের ভ্যারিয়েবলে রাখা হলো।
  2. একটি নতুন class. compiler এই lambda-র জন্য একটি অনন্য class তৈরি করে। এর নাম আপনি জানেন না, লিখতেও পারবেন না — তাই auto লাগে। দুটি অক্ষরে-অক্ষরে এক lambda-ও দুটি আলাদা টাইপ।
  3. capture হয় data member. [limit] মানে class-এ একটি int limit সদস্য, আর closure তৈরির মুহূর্তে বাইরের limit-এর মান সেখানে কপি হয়। reference capture হলে সদস্যটি হতো একটি reference — সেটিই পরের অংশের সব ঝামেলার উৎস।
  4. body হয় operator(). আপনার প্যারামিটার আর body হুবহু operator()-এ চলে যায়, আর ফেরত-টাইপ return থেকে ঠিক হয়। খেয়াল করুন শেষের const: এটি একটি const member function, তাই body থেকে কপি করা limit বদলানো যায় না। বদলাতে চাইলে mutable লাগবে (অংশ ৪)।
  5. তৈরি আর ডাক। lambda লেখার জায়গায় closure object তৈরি হয়, আর pass(45) হলো operator()-এর সাধারণ ডাক। এখানে কোনো virtual, কোনো pointer, কোনো heap নেই — compiler সরাসরি এই ডাক inline করে দিতে পারে। এজন্যই lambda-সহ std::sort প্রায়ই C-এর qsort-এর চেয়ে দ্রুত। (ছবিটি সরল করা; আসল closure-এর constructor আপনি নিজে ডাকতে পারেন না।)

এই ছবিটি নিজের চোখে দেখতে চাইলে Andreas Fertig-এর বানানো অনলাইন টুল C++ Insights-এ যেকোনো lambda লিখুন — সেটি ঠিক এমন একটি class দেখায়। একবার এই রূপান্তর বুঝে গেলে lambda-র প্রায় সব নিয়ম — কেন auto লাগে, কেন ডিফল্টে বদলানো যায় না, কেন reference capture বিপজ্জনক — যুক্তি দিয়ে নিজেই বের করতে পারবেন।

একটি ছোট খুঁটিনাটি

C++17 থেকে lambda সম্ভব হলে নিজে থেকেই constexpr, অর্থাৎ compile-এর সময়েও চালানো যায় (পর্ব ৩৯)। আর C++20 থেকে capture-ছাড়া lambda-র closure টাইপের ডিফল্ট constructor আছে, তাই decltype দিয়ে টাইপটি নিয়ে নতুন object বানানো যায় — যেমন std::set-এর comparator টাইপ হিসেবে। শুরুতে এগুলো না জানলেও চলে।

৪ · Capture — কী সঙ্গে যাবে, আর কীভাবে

lambda-র body শুধু তিন রকম জিনিস দেখতে পায়: নিজের প্যারামিটার, নিজের ভেতরে ঘোষিত ভ্যারিয়েবল, আর capture করা জিনিস। (global ভ্যারিয়েবল আর static ভ্যারিয়েবলও দেখা যায়, capture ছাড়াই — কারণ সেগুলো সবসময় বেঁচে থাকে।) বাইরের একটি local ভ্যারিয়েবল ব্যবহার করতে চাইলে তাকে capture list-এ নাম দিয়ে আনতে হয়। এখানে দুটি মৌলিক পথ:

Capture by value — [x]

closure তৈরির মুহূর্তে x-এর একটি কপি নেয়। পরে বাইরের x বদলালেও closure-এর কপি বদলায় না, আর বাইরের x মরে গেলেও closure নিরাপদ।

snapshot · ছবি তুলে রাখা

Capture by reference — [&x]

কোনো কপি নয় — আসল x-এর সাথে একটি reference. বাইরের পরিবর্তন closure দেখতে পায়, আর closure বদলালে বাইরেরটাই বদলায়। কিন্তু x মরে গেলে closure-এর হাতে থাকে একটি মৃত ঠিকানা।

live link · সরাসরি সংযোগ

Capture by value হলো সেই চিরকুটে সংখ্যাটি লিখে দেওয়া; capture by reference হলো চিরকুটে লেখা “অফিসের হোয়াইটবোর্ডে যা লেখা আছে, সেটা দেখে নেবেন।” দ্বিতীয়টি সবসময় সর্বশেষ মান দেখায় — যতক্ষণ হোয়াইটবোর্ডটি আছে। একটি ফলাফল-প্রকাশের দৃশ্যে দুটির পার্থক্য দেখুন: পাসের নম্বর আগে ৪০ ধরা হয়েছিল, পরে বোর্ড সিদ্ধান্ত নিল ৫০.

#include <print>

int main() {
    int threshold = 40;
    auto byValue = [threshold](int m) { return m >= threshold; };   // copy now
    auto byRef   = [&threshold](int m) { return m >= threshold; };  // live link
    threshold = 50;                                                  // rule changes
    std::println("45 passes? value: {}, ref: {}", byValue(45), byRef(45));

    int calls = 0;
    auto counter = [calls]() mutable { return ++calls; };            // own copy
    counter();
    counter();
    std::println("counter: {}, outer calls: {}", counter(), calls);
}

আউটপুট দুই লাইন: 45 passes? value: true, ref: false আর counter: 3, outer calls: 0. প্রথম লাইন: byValue তৈরির সময় ৪০ কপি করে রেখেছিল, তাই ৪৫ তার চোখে পাস। byRef প্রতিবার আসল threshold দেখে, যা এখন ৫০ — তাই ৪৫ ফেল। একই নিয়ম, একই ইনপুট, ভিন্ন উত্তর — কারণ শুধু capture-এর ধরন। দ্বিতীয় অংশে mutable lambda নিজের কপি calls তিনবার বাড়িয়েছে (১, ২, ৩), কিন্তু বাইরের calls শূন্যই রয়ে গেছে — closure-এর ভেতরের কপি আর বাইরের ভ্যারিয়েবল দুটি আলাদা জিনিস।

প্রথম অংশটি চলার সময় প্রতিটি ধাপে কী ঘটে, দেখুন। প্রতিটি ধাপে আগে নিজে অনুমান করুন:

1int threshold = 40;2auto byValue = [threshold](int m) { ... };3auto byRef = [&threshold](int m) { ... };4threshold = 50;5println(..., byValue(45), byRef(45));
মেমরিতে এই মুহূর্তে
threshold
byValue-এর ভেতরের কপি
byRef-এর ভেতরে
byRef যা দেখে
  1. একটি সাধারণ ভ্যারিয়েবল। threshold-এ ৪০. এখনো কোনো lambda নেই — শুধু main-এর stack-এ একটি int.
  2. byValue তৈরি — ছবি তোলা হলো। closure object তৈরির মুহূর্তে তার ভেতরের int threshold সদস্যে ৪০ কপি হলো। এখন থেকে byValue-এর কাছে নিজের একটি সংখ্যা আছে, বাইরের ভ্যারিয়েবলের সাথে আর কোনো সম্পর্ক নেই।
  3. byRef তৈরি — সংযোগ পাতা হলো। এই closure-এর ভেতরে কোনো সংখ্যা নেই, আছে বাইরের threshold-এর একটি reference (বাস্তবে সাধারণত তার ঠিকানা)। সে যা দেখে, তা আসলে বাইরের ভ্যারিয়েবলেরই মান।
  4. নিয়ম বদলাল। threshold = 50. বাইরের ভ্যারিয়েবল বদলাল, তাই byRef-ও এখন ৫০ দেখে। কিন্তু byValue-এর কপি আগের ৪০-এই আটকে আছে — কেউ তাকে খবর দেয়নি, দেওয়ার উপায়ও নেই।
  5. ডাকার মুহূর্তে ফল। byValue(45): ৪৫ ≥ ৪০, true. byRef(45): ৪৫ ≥ ৫০, false. কোনটি ঠিক? প্রশ্নটা প্রসঙ্গের। “ফল প্রকাশের দিনের নিয়ম মানতে হবে” হলে value; “সবসময় বর্তমান নিয়ম” হলে reference — কিন্তু তখন নিশ্চিত হতে হবে যে threshold lambda-র চেয়ে বেশি দিন বাঁচবে।

সব capture-এর রূপ এক জায়গায়

প্রতিটি ভ্যারিয়েবলের নাম আলাদা করে না লিখে একবারে বলে দেওয়ার উপায়ও আছে — একে বলে default capture. পুরো তালিকা:

লেখামানেকখন
[]কিছুই capture নয়নিয়মটি শুধু প্যারামিটারের ওপর নির্ভর করে; function pointer-এ রূপান্তর করা যায়
[x]x-এর কপিছোট মান (int, double), বা lambda বেশি দিন বাঁচবে
[&x]x-এর referenceবড় object কপি না করতে, বা x বদলাতে — আর lambda x-এর আগেই শেষ হবে
[=]body-তে যা ব্যবহার হয়, সব কপিসুবিধাজনক, কিন্তু কী কী কপি হলো চোখে পড়ে না
[&]body-তে যা ব্যবহার হয়, সব referenceসঙ্গে সঙ্গে চলে শেষ হয় এমন lambda-তে (যেমন algorithm-এ) নিরাপদ; সংরক্ষিত হলে বিপদ
[=, &total]সব কপি, শুধু total referenceডিফল্ট আর ব্যতিক্রম মেশানো
[n = expr]init-capture: নতুন সদস্য n, মান exprC++14 থেকে; move করে আনা বা হিসাব করা মান রাখা
[this]বর্তমান object-এর pointermember function-এর ভেতরে; object lambda-র চেয়ে বেশি দিন বাঁচলে
[*this]বর্তমান object-এর পুরো কপিC++17 থেকে; lambda object-এর চেয়ে বেশি দিন বাঁচতে পারে

Init-capture — সঙ্গে নতুন কিছু বানিয়ে নেওয়া

C++11-এর capture শুধু বিদ্যমান ভ্যারিয়েবল কপি বা reference করতে পারত। ধরুন একটি বড় std::vector lambda-র ভেতরে নিতে চান, কিন্তু কপি করতে চান না — সরিয়ে (move) নিতে চান। অথবা capture-এর মুহূর্তে একটি হিসাব করে রাখতে চান। C++14 এনেছে init-capture: capture list-এ একটি নতুন নাম আর তার প্রাথমিক মান।

std::vector<int> orders = loadOrders();              // ধরুন লাখখানেক উপাদান
auto report = [list = std::move(orders)] {             // কপি নয়, সরিয়ে নেওয়া
    return list.size();
};                                                     // orders এখন খালি (moved-from)

auto upload = [file = std::make_unique<Log>()] { /* file closure-এর মালিকানায় */ };
auto withTax = [rate = vat / 100.0](double p) { return p * (1 + rate); };  // হিসাব একবার

std::move-এর বিস্তারিত পর্ব ৩৬-এ। আপাতত জানুন: [list = std::move(orders)] মানে closure-এর ভেতরে list নামের একটি সদস্য, যা orders-এর ডেটা নিজের করে নিল। std::unique_ptr-এর মতো move-only type, যার কপিই হয় না, lambda-তে নেওয়ার একমাত্র পরিষ্কার পথ এটিই। (আর এমন lambda নিজেও কপি করা যায় না — এটা পরে std::function-এর আলোচনায় গুরুত্বপূর্ণ হবে।)

mutable — নিজের কপি বদলানোর অনুমতি

অংশ ৩-এ দেখেছেন, closure-এর operator() ডিফল্টভাবে const. তাই [calls] { return ++calls; } compile হয় না — const ফাংশনে সদস্য বদলানো নিষেধ। mutable লিখলে operator() থেকে const সরে যায়, আর lambda নিজের কপি বদলাতে পারে — যেমন উপরের counter. মনে রাখুন: এটি বাইরের ভ্যারিয়েবল বদলায় না, বদলায় closure-এর নিজের সদস্য। বাইরেরটা বদলাতে চাইলে capture by reference. আর একটি সূক্ষ্ম ফাঁদ: mutable lambda কপি করলে দুটি কপি আলাদা গোনে, কারণ প্রত্যেকের নিজের সদস্য আছে। কোনো algorithm যদি আপনার lambda-কে ভেতরে কপি করে নেয়, আপনার counter হয়তো আপনার হাতের কপিতে বাড়বেই না।

this capture — member function-এর ভেতরে lambda

class-এর member function-এর ভেতরে lambda থেকে সদস্য ব্যবহার করতে হলে this capture করতে হয়। [this] কপি করে শুধু this pointer — object-টি নয়। তাই lambda object-এর চেয়ে বেশি দিন বাঁচলে আবারও সেই মৃত ঠিকানার সমস্যা। C++17-এর [*this] পুরো object কপি করে নেয়, যা পরে চলবে এমন কাজে নিরাপদ। একটি পুরোনো ফাঁদ: member function-এর ভেতরে [=] লিখলে this চুপচাপ capture হয় — দেখতে “সব কপি”, আসলে object-এর ঠিকানা। C++20 এই নিঃশব্দ this-capture-কে deprecated করেছে (এখনো compile হয়, তবে compiler সতর্ক করতে পারে); এখন স্পষ্ট লিখুন [=, this] বা [*this].

struct Shop {
    int discount = 10;
    auto priceRule() {
        return [this](int p) { return p - p * discount / 100; };   // Shop-এর ঠিকানা
    }
    auto priceRuleCopy() {
        return [*this](int p) { return p - p * discount / 100; };  // Shop-এর কপি (C++17)
    }
};

৫ · Dangling capture — যে lambda তার ভ্যারিয়েবলের চেয়ে বেশি দিন বাঁচে

পর্ব ১৮-এ আপনি lifetime আর dangling reference দেখেছেন: যে reference-এর object মরে গেছে, তা দিয়ে পড়া বা লেখা undefined behavior. lambda-তে এই বিপদ লুকিয়ে থাকে আরও ভালোভাবে, কারণ reference-টি কোডে কোথাও চোখে পড়ে না — সেটি থাকে একটি & চিহ্নের আড়ালে, closure-এর ভেতরে।

নিয়মটি সরল: reference capture তখনই নিরাপদ, যখন lambda তার capture করা ভ্যারিয়েবলের আগে শেষ হয়। count_if-এর ভেতরে দেওয়া lambda সঙ্গে সঙ্গে চলে আর algorithm শেষ হলে বাতিল হয় — সেখানে [&] নিরাপদ। কিন্তু যে lambda সংরক্ষিত হয় — একটি callback তালিকায়, একটি thread-এ, একটি ফাংশন থেকে ফেরত দিয়ে — সেটি তৈরির জায়গার scope-এর পরেও বাঁচে। একটি অ্যাপের বাটনে ক্লিক গোনার উদাহরণে দেখুন (এই কোড চালাবেন না):

std::vector<std::function<void()>> onClick;   // বাটনের handler তালিকা

void addCounter() {
    int clicks = 0;                                  // addCounter-এর local
    onClick.push_back([&clicks] { ++clicks; });      // reference সংরক্ষিত হলো!
}                                                    // clicks এখানে ধ্বংস

addCounter();
// ... কিছুক্ষণ পরে ব্যবহারকারী বাটন চাপলেন ...
onClick[0]();          // মৃত clicks-এ লেখা: undefined behavior
1std::vector<std::function<void()>> onClick;2void addCounter() {3    int clicks = 0;4    onClick.push_back([&clicks] { ++clicks; });5}6onClick[0]();   // পরে, বাটন চাপলে
মেমরির অবস্থা
clicks
closure-এর ভেতরে
অবস্থা
  1. ভ্যারিয়েবলের জন্ম। addCounter() ডাকা হলো, তার stack frame-এ clicks তৈরি হলো। এর জীবন এই ফাংশনের সমান — ফাংশন শেষ, clicks শেষ।
  2. lambda সংরক্ষণ। [&clicks] lambda-টি onClick তালিকায় ঢুকল। closure-এর ভেতরে আছে clicks-এর reference. এই মুহূর্তে সব ঠিক — clicks এখনো জীবিত। কিন্তু তালিকাটি global, তাই closure-টি অনেক দিন বাঁচবে।
  3. ফাংশন শেষ — নিঃশব্দ বিপদ। }-এ পৌঁছাতেই stack frame বাতিল, clicks ধ্বংস। কিন্তু closure জানে না; তার reference এখনো সেই পুরোনো ঠিকানার দিকে তাকিয়ে আছে। compiler সাধারণত এখানে কোনো সতর্কবার্তা দেয় না, কারণ std::function-এর ভেতরে গিয়ে কী ঘটল তা সে দেখে না।
  4. বাটন চাপা — undefined behavior. পরে handler চলল আর ++clicks লিখল এমন এক মেমরিতে, যা এখন হয়তো অন্য কোনো ফাংশনের ভ্যারিয়েবল। ফল অনুমান করা যায় না: কখনো দিব্যি চলে, কখনো অন্য ডেটা নষ্ট হয়, কখনো crash. সবচেয়ে খারাপ হলো “দিব্যি চলা” — বাগটি তখন মাসের পর মাস লুকিয়ে থাকে।
  5. সমাধান — কপি নিয়ে যান। [clicks]() mutable { ++clicks; }: closure-এর নিজের একটি clicks, যা closure-এর সমান বাঁচে। গোনা ভাগ করে ব্যবহার করতে হলে মালিকানাসহ শেয়ার করুন: [c = std::make_shared<int>(0)] { ++*c; } (পর্ব ১৭)। নিয়ম: যে lambda সংরক্ষিত হবে, সে reference নয়, মান বা মালিকানা নিয়ে যাবে।

একই ভুলের আরও দুটি পরিচিত রূপ আছে। প্রথমটি ফাংশন থেকে lambda ফেরত দেওয়া, যেখানে local ভ্যারিয়েবল reference-এ ধরা: return [&rate](int p) { ... }; — ফাংশন ফেরত দেওয়ার সঙ্গে সঙ্গে rate মরে। দ্বিতীয়টি thread: একটি std::thread-কে [&] lambda দিয়ে চালিয়ে detach করলে, মূল ফাংশন আগে শেষ হয়ে গেলে thread মৃত ভ্যারিয়েবল পড়ে (পর্ব ৪৫)। তিনটি ক্ষেত্রেই প্রশ্ন একটাই: এই lambda কতক্ষণ বাঁচবে, আর আমার capture করা জিনিস কতক্ষণ? উত্তর অনিশ্চিত হলে value capture বেছে নিন। AddressSanitizer (-fsanitize=address, পর্ব ১৮) চালিয়ে পরীক্ষা করলে এমন ভুলের অনেকগুলো ধরা পড়ে।

[&] দেখতে নির্দোষ — “সব reference, কপির খরচ নেই।” কিন্তু এটি প্রতিটি ব্যবহৃত local ভ্যারিয়েবলকে নিঃশব্দে reference বানায়, আর পরে কেউ body-তে নতুন একটি ভ্যারিয়েবল ব্যবহার করলে সেটিও reference-এ ধরা পড়ে — কোনো লেখা ছাড়াই। সংরক্ষিত বা অন্য thread-এ চলা lambda-তে [&] লিখবেন না; প্রতিটি capture নাম ধরে লিখুন, যাতে কোডটি নিজেই বলে দেয় কী কীভাবে যাচ্ছে।

৬ · Generic lambda আর STL algorithm-এ lambda

প্যারামিটারে auto — একটি lambda, অনেক টাইপ

C++14 থেকে lambda-র প্যারামিটারে auto লেখা যায়। একে বলে generic lambda. তখন closure-এর operator() নিজেই একটি template হয়ে যায় (পর্ব ২৮) — প্রতিটি নতুন টাইপে ডাকলে compiler একটি নতুন সংস্করণ বানায়।

auto twice = [](auto x) { return x + x; };
twice(4);                       // 8      — int সংস্করণ
twice(2.5);                     // 5.0    — double সংস্করণ
twice(std::string("ha"));       // "haha" — string সংস্করণ

// compiler-এর চোখে closure-এর ভেতরটা মোটামুটি এমন:
// template <typename T> auto operator()(T x) const { return x + x; }

C++20 আরেক ধাপ এগিয়ে template lambda এনেছে: capture list-এর পরে সরাসরি template প্যারামিটার। এটি কাজে লাগে যখন টাইপটির নাম body-তে দরকার, বা দুটি প্যারামিটার একই টাইপের হতেই হবে:

// T-কে নাম দিয়ে ধরা: vector-এর উপাদানের টাইপ জানা দরকার
auto firstOr = []<typename T>(const std::vector<T>& v, T fallback) {
    return v.empty() ? fallback : v.front();
};
firstOr(std::vector{3, 1, 2}, 0);    // 3 — T = int

STL algorithm-এ lambda — যেখানে lambda সবচেয়ে বেশি দেখা যায়

পর্ব ৩৩-এর প্রায় প্রতিটি algorithm একটি callable নেয়। lambda নেওয়ার জন্য যেসব ফাংশন বানানো, তাদের সাধারণ নাম higher-order function — যে ফাংশন অন্য ফাংশনকে প্যারামিটার হিসেবে নেয়। তিনটি পরিচিত ব্যবহার একটি দোকানের উদাহরণে দেখুন — দাম অনুযায়ী পণ্য সাজানো (দাম সমান হলে নাম অনুযায়ী), ঈদের সেলের ছাড় হিসাব, আর ৩০০ টাকার কম দামের পণ্য গোনা:

#include <algorithm>
#include <print>
#include <string>
#include <vector>

struct Product { std::string name; int price; };

auto make_discount(int percent) {                          // returns a lambda
    return [percent](int price) { return price - price * percent / 100; };
}

int main() {
    std::vector<Product> items{{"pen", 20}, {"bag", 900}, {"cap", 250}, {"ink", 20}};
    std::ranges::sort(items, [](const Product& a, const Product& b) {
        if (a.price != b.price) return a.price < b.price;  // cheaper first
        return a.name < b.name;                             // tie: by name
    });
    auto eidSale = make_discount(10);
    for (const auto& p : items) std::println("{}: {} -> {}", p.name, p.price, eidSale(p.price));
    auto cheap = std::ranges::count_if(items, [](const Product& p) { return p.price < 300; });
    std::println("under 300: {}", cheap);
}

আউটপুট পাঁচ লাইন: ink: 20 -> 18, pen: 20 -> 18, cap: 250 -> 225, bag: 900 -> 810, আর under 300: 3. comparator-টি দুই ধাপে সিদ্ধান্ত নেয়: দাম আলাদা হলে কম দাম আগে; সমান হলে (pen আর ink দুটিই ২০) নাম অনুযায়ী, তাই ink আগে। make_discount(10) একটি lambda ফেরত দিয়েছে, যার ভেতরে ১০ কপি করা আছে — সেটি eidSale-এ রেখে প্রতিটি পণ্যে ডাকা হলো (২৫% ছাড়ের সেল হলে লিখতেন make_discount(25), কোড একই)। শেষে count_if একটি predicate দিয়ে গুনেছে কয়টির দাম ৩০০-এর কম। পুরো প্রোগ্রামে একটিও নাম দেওয়া নিয়ম-ফাংশন নেই, অথচ প্রতিটি নিয়ম ঠিক তার ব্যবহারের পাশে লেখা।

comparator লেখার সময় একটি নিয়ম মানতেই হবে: এটি “কঠোরভাবে কম” (<) বোঝাবে, কখনো <= নয়। <= লিখলে সমান দুটি উপাদানে “a আগে” আর “b আগে” দুটিই সত্য হয়, যা sort-এর শর্ত (strict weak ordering) ভাঙে — ফল undefined behavior: ভুল ক্রম থেকে crash পর্যন্ত যেকোনো কিছু। আর দাম-তারপর-নাম ধরনের তুলনা আরও সংক্ষেপে লেখা যায় std::tie(a.price, a.name) < std::tie(b.price, b.name) দিয়ে।

শিক্ষার্থীর নম্বর থেকে পাসের সংখ্যা গোনা, বা প্রত্যেককে একটি করে বার্তা দেখানো — দুটিই এক লাইনে:

int passMark = 40;                                   // চলার সময় ঠিক হতে পারে
auto passed = std::ranges::count_if(marks, [passMark](int m) { return m >= passMark; });

int total = 0;
std::ranges::for_each(marks, [&total](int m) { total += m; });   // [&] এখানে নিরাপদ:
                                                                 // lambda এখনই চলে শেষ

এখানে std::ranges::count_if আর std::ranges::for_each দুটিই lambda সঙ্গে সঙ্গে চালিয়ে শেষ করে, তাই [&total] সম্পূর্ণ নিরাপদ — অংশ ৫-এর নিয়মটি মিলিয়ে দেখুন। (মোট যোগফলের জন্য অবশ্য std::accumulate বা std::ranges::fold_left আরও পরিষ্কার; for_each এখানে শুধু reference capture দেখাতে।) পরের পর্বের ranges আর views-এ lambda আরও কেন্দ্রীয় — সেখানে filter আর transform-কে শুধু lambda দিয়েই বলা হয় কী করতে হবে।

৭ · Callable রাখা — auto, std::function, না function pointer?

lambda একবার লিখে সঙ্গে সঙ্গে algorithm-এ দেওয়া এক কথা। কিন্তু অনেক সময় একে কোথাও রাখতে হয় — একটি ভ্যারিয়েবলে, একটি class-এর সদস্যে, একটি তালিকায়। আর প্রতিটি closure-এর টাইপ আলাদা ও নামহীন হওয়ায় প্রশ্ন ওঠে: সেই ভ্যারিয়েবলের টাইপ কী লিখবেন? তিনটি উত্তর:

পথকী রাখতে পারেখরচকখন
auto বা template প্যারামিটারঠিক সেই একটি closure, তার আসল টাইপেকোনো বাড়তি খরচ নেই; inline হয়local ভ্যারিয়েবল, ফাংশনে callable পাঠানো (template <class F>)
std::function<R(Args)>এই স্বাক্ষরের যেকোনো callable: ফাংশন, lambda, functorপরোক্ষ ডাক; বড় capture হলে heap; সাধারণত inline হয় নানানা ধরনের callable এক টাইপে রাখতে: callback তালিকা, class-এর সদস্য
function pointerসাধারণ ফাংশন, বা capture-ছাড়া lambdaপরোক্ষ ডাক, কিন্তু খুব হালকাC লাইব্রেরি বা C API-তে callback দিতে

std::function — যেকোনো callable-এর একটি বাক্স

std::function (হেডার <functional>) একটি “বাক্স”, যার গায়ে শুধু স্বাক্ষর লেখা — যেমন std::function<int(int)> মানে “int নেয়, int দেয় এমন যেকোনো কিছু।” ভেতরে কী আছে — সাধারণ ফাংশন, lambda, নাকি পর্ব ২৩-এর functor — বাইরে থেকে বোঝা যায় না, দরকারও নেই। আসল টাইপ লুকিয়ে একটি সাধারণ টাইপ দেখানোর এই কৌশলকে বলে type erasure. এর দাম তিনটি: ডাকটি পরোক্ষ (ভেতরে virtual ফাংশনের মতো একটি ব্যবস্থা), তাই compiler সাধারণত inline করতে পারে না; capture বড় হলে closure রাখতে heap থেকে মেমরি নিতে হতে পারে (ছোট হলে অনেক বাস্তবায়ন বাক্সের ভেতরেই রাখে — small buffer optimization); আর বাক্সটি খালিও হতে পারে। একটি std::function ভ্যারিয়েবল তার জীবনে কী কী ধরে রাখতে পারে, দেখুন:

1std::function<int(int)> price;      // খালি2price = half;                        // int half(int p)3price = [](int p) { return p - 10; };4price = [pct](int p) { return p - p * pct / 100; };5price = Tax{15};                     // functor6price = nullptr;                     // আবার খালি
বাক্সের ভেতরে (pct = 20)
ভেতরে কী
if (price)
price(200)
  1. খালি বাক্স। শুধু ঘোষণা করলে std::function খালি থাকে। if (price) false দেয়। খালি অবস্থায় ডাকলে কোনো undefined behavior নয়, বরং exception: std::bad_function_call. তাই অন্যের দেওয়া callback ডাকার আগে if (cb) দিয়ে দেখে নেওয়া ভালো অভ্যাস।
  2. সাধারণ ফাংশন। int half(int p) { return p / 2; } — নামটি দিলেই হলো। বাক্সে একটি function pointer রাখা হলো; price(200) দেয় 100.
  3. capture-ছাড়া lambda. একই ভ্যারিয়েবলে এবার সম্পূর্ণ ভিন্ন টাইপ — একটি closure. বাক্সের টাইপ বদলায়নি, শুধু ভেতরের জিনিস বদলেছে। 200 − 10 = 190.
  4. capture-সহ lambda. closure-টি নিজের ভেতরে pct-এর কপি নিয়ে এসেছে, আর বাক্সকে সেটিও রাখতে হবে। ছোট capture বলে অনেক বাস্তবায়নে এটি বাক্সের ভেতরের ছোট জায়গাতেই আঁটে; বড় capture হলে heap লাগত। 200 − 40 = 160.
  5. পর্ব ২৩-এর functor. struct Tax { int rate; int operator()(int p) const { return p + p * rate / 100; } }; — যেকোনো object, যার মানানসই operator() আছে, এই বাক্সে আঁটে। 200 + 30 = 230. চার রকম টাইপ, এক ভ্যারিয়েবল — এটাই type erasure.
  6. আবার খালি। nullptr দিলে বাক্স খালি হয়, আর ভেতরে রাখা Tax object ধ্বংস হয়। function pointer-এর মতো std::function-ও “কিছু না” হতে পারে — যা ঐচ্ছিক callback-এর জন্য সুবিধা, কিন্তু না দেখে ডাকলে exception. auto ভ্যারিয়েবলে রাখা lambda কখনো খালি হয় না।

একটি GUI-ধাঁচের ঘটনা-ব্যবস্থা

std::function-এর সবচেয়ে স্বাভাবিক ব্যবহার হলো callback তালিকা — যেখানে নানা জায়গা থেকে নানা ধরনের lambda এসে একটি ঘটনার জন্য অপেক্ষা করে। একটি অনলাইন দোকানের “কার্টে যোগ করুন” বাটনের কথা ভাবুন: ক্লিক হলে কার্টের সংখ্যা বাড়বে, একটি বার্তা দেখাবে, আর analytics-এ একটি গণনা যাবে। তিনটি কাজ তিনজন আলাদা প্রোগ্রামার লিখেছেন, তিনটি আলাদা closure টাইপ — তবু এক তালিকায়:

struct Button {
    std::vector<std::function<void()>> onClick;     // event handler-এর তালিকা
    void click() const { for (const auto& h : onClick) h(); }
};

Cart cart;                                           // buy-এর চেয়ে বেশি দিন বাঁচে
Button buy;
buy.onClick.push_back([&cart] { cart.add(); });      // reference: cart বেঁচে থাকবে তো?
buy.onClick.push_back([] { std::println("added to cart"); });
buy.onClick.push_back([tag = std::string("buy-btn")] { track(tag); });  // নিজের কপি
buy.click();                                         // তিনটি handler পরপর চলে

এখানে প্রতিটি event handler-এর capture-সিদ্ধান্ত আলাদা, আর প্রতিটির পেছনে lifetime-এর একটি প্রশ্ন আছে। [&cart] ঠিক আছে শুধু যদি cart বাটনের চেয়ে বেশি দিন বাঁচে — এখানে cart আগে তৈরি, তাই পরে ধ্বংস হবে (পর্ব ২১-এর উল্টো ক্রম)। এই ধরনের নকশায় std::function প্রায় অপরিহার্য, কারণ তালিকার প্রতিটি উপাদানের টাইপ এক হতে হয়। কিন্তু একটি local lambda রাখতে, বা একটি ফাংশনে callable পাঠাতে std::function দরকার নেই — সেখানে auto বা template প্যারামিটার যথেষ্ট, আর দ্রুততর। আরেকটি সীমা: std::function-এ রাখা জিনিস কপি করা যায় এমন হতে হয়। তাই unique_ptr capture করা lambda এতে আঁটে না; C++23-এর std::move_only_function সেই ফাঁক পূরণ করে।

function pointer — capture-ছাড়া lambda-র বিশেষ সুবিধা

যে lambda কিছুই capture করে না, তার closure-এ কোনো data member নেই — ফলে তার operator()-এর কোনো object-এর দরকারই নেই। তাই C++ নিয়ম করেছে: capture-ছাড়া lambda নিজে থেকেই একই স্বাক্ষরের function pointer-এ রূপান্তরিত হয়। এটি C লাইব্রেরির সাথে কাজ করতে খুব কাজের, কারণ C শুধু function pointer চেনে:

int (*op)(int, int) = [](int a, int b) { return a + b; };   // ঠিক আছে
// int (*bad)(int) = [k](int a) { return a + k; };          // error: capture আছে

std::qsort(arr, n, sizeof(int), [](const void* a, const void* b) {
    int x = *static_cast<const int*>(a), y = *static_cast<const int*>(b);
    return (x > y) - (x < y);                                // C-এর qsort-এ lambda
});

capture-সহ lambda-র state রাখার একটি জায়গা দরকার, আর একটি function pointer শুধু কোডের একটি ঠিকানা — সেখানে ডেটা রাখার জায়গা নেই। তাই সেটি রূপান্তর হয় না। C API-গুলো এই সমস্যা সমাধান করে একটি বাড়তি void* প্যারামিটার দিয়ে, যার মাধ্যমে আপনি নিজের ডেটার ঠিকানা পাঠান — ভাবনার প্রশ্ন ০৪-এ বিস্তারিত।

C জানলে

C-তে নিয়ম পাঠানোর একমাত্র পথ function pointer, আর সাথে কোনো তথ্য পাঠাতে চাইলে আলাদা একটি void* — যেমন qsort-এর comparator বা thread-এর start ফাংশন। C++-এর lambda সেই নিয়ম আর তথ্যকে এক object-এ বাঁধে, টাইপ-নিরাপদভাবে, আর inline হওয়ার সুযোগও রাখে। capture-ছাড়া lambda সরাসরি C-এর function pointer হিসেবে চলে, তাই পুরোনো C লাইব্রেরির সাথে মেশাতে কোনো বাধা নেই। C-এর হাতে-লেখা callback দেখতে C কোর্সের ফাংশন পয়েন্টার ও callback পর্ব পড়ুন।

৮ · lambda ফেরত দেওয়া, সঙ্গে সঙ্গে ডাকা, আর নিজেকে ডাকা

Factory — ফাংশন থেকে lambda ফেরত

অংশ ৬-এর make_discount একটি ছোট কিন্তু শক্তিশালী ধারণা দেখিয়েছে: একটি ফাংশন যা নিয়ম বানিয়ে দেয়। C++14 থেকে ফেরত-টাইপে auto লিখলেই lambda ফেরত দেওয়া যায় — আসল টাইপের নাম লিখতে হয় না। একই factory থেকে ভিন্ন নিয়ম:

auto make_discount(int percent) {
    return [percent](int p) { return p - p * percent / 100; };   // percent কপি: নিরাপদ
}
auto eid    = make_discount(10);     // প্রতিটি closure-এ নিজের percent
auto boishakh = make_discount(15);
auto bad = [](int percent) {
    return [&percent](int p) { return p - p * percent / 100; };  // ভুল: percent মরবে
};

ফেরত দেওয়া lambda প্রায় সবসময় সংরক্ষিত হয়, তাই এখানে reference capture প্রায় সবসময় ভুল — উপরের bad-এ percent একটি প্যারামিটার, ফাংশন শেষ হলেই যা ধ্বংস হয়। factory-তে সবসময় value বা init-capture. ফাংশন যদি নানা পথে নানা lambda ফেরত দেয় (যেমন if-else-এ দুটি আলাদা lambda), তাহলে auto চলবে না, কারণ দুটির টাইপ আলাদা; তখন ফেরত-টাইপ হিসেবে std::function লাগে।

সঙ্গে সঙ্গে ডাকা lambda — জটিল const মান বানাতে

কখনো একটি ভ্যারিয়েবলের মান ঠিক করতে কয়েক লাইনের যুক্তি লাগে — if-else, লুপ। সাধারণ পথে লিখতে হয় আগে ভ্যারিয়েবল ঘোষণা, তারপর শাখায় শাখায় মান বসানো — ফলে ভ্যারিয়েবলটি const করা যায় না। সমাধান: একটি lambda লিখে সঙ্গে সঙ্গে ডাকা — IIFE (immediately invoked function expression):

const std::string grade = [&] {       // শেষে () — লিখেই ডাকা
    if (marks >= 80) return "A+";
    if (marks >= 70) return "A";
    if (marks >= 40) return "Pass";
    return "Fail";
}();                                  // grade এখন const, আর কখনো বদলাবে না

এখানে [&] নিরাপদ, কারণ lambda তৈরি হয়েই চলে আর শেষ হয়ে যায় — কোথাও সংরক্ষিত হয় না। কোডটি বলে দেয়: “grade একবারেই ঠিক হবে, তারপর আর কেউ বদলাবে না।” ভুল করে পরে কেউ grade-এ নতুন মান বসাতে চাইলে compiler আটকাবে।

নিজেকে ডাকা — recursive lambda

lambda-র নিজের কোনো নাম নেই, তাই body থেকে নিজেকে ডাকা সহজ নয় — auto fact = [](int n) { ... fact(n - 1) ... }; compile হয় না, কারণ fact-এর টাইপ তখনো ঠিকই হয়নি। পুরোনো সমাধান ছিল std::function-এ রেখে reference-এ নিজেকে capture করা (ধীর), বা নিজেকে একটি প্যারামিটার হিসেবে পাঠানো। C++23-এর “deducing this” (explicit object parameter) এটি পরিষ্কার করেছে:

// C++23: প্রথম প্যারামিটার "this auto self" — lambda নিজেই
auto fact = [](this auto self, int n) -> long long {
    return n <= 1 ? 1 : n * self(n - 1);
};
fact(10);    // 3628800

// C++14 পথ: নিজেকে প্যারামিটার হিসেবে পাঠানো
auto fib = [](auto& me, int n) -> int { return n < 2 ? n : me(me, n - 1) + me(me, n - 2); };
fib(fib, 10);   // 55

তবে সত্যি বলতে, recursive lambda বাস্তবে কম দরকার হয়। নিয়মটি নিজেকে ডাকার মতো জটিল হলে প্রায়ই একটি নাম দেওয়া ফাংশনই বেশি পরিষ্কার।

std::bind — পুরোনো পথ, এখন এড়িয়ে চলুন

C++11-এর সাথে std::bind-ও এসেছিল (Boost.Bind থেকে): একটি ফাংশনের কিছু argument আগে থেকে বসিয়ে নতুন callable বানানো। কিন্তু একই কাজ lambda দিয়ে বেশি পরিষ্কার, আর প্রায়ই দ্রুত:

using namespace std::placeholders;
auto addVat1 = std::bind(addTax, _1, 15);             // _1 মানে? কোনটা কোথায় যায়?
auto addVat2 = [](int price) { return addTax(price, 15); };   // সরাসরি পড়া যায়

bind-এর placeholder (_1, _2) পড়তে কঠিন, overload করা ফাংশনে বিভ্রান্ত হয়, আর বাড়তি argument নীরবে ফেলে দেয়। Scott Meyers তাঁর Effective Modern C++ বইয়ে সোজাসুজি বলেছেন: std::bind-এর চেয়ে lambda বেছে নিন। পুরোনো কোডে দেখলে চিনে রাখুন, নতুন কোডে লিখবেন না।

“lambda” নামটি এসেছে গণিত থেকে। ১৯৩০-এর দশকে আমেরিকার গণিতবিদ Alonzo Church তৈরি করেন lambda calculus — একটি গাণিতিক ব্যবস্থা, যেখানে সবকিছুই নাম-ছাড়া ফাংশন, আর ফাংশন লেখা হয় গ্রিক অক্ষর λ দিয়ে। ১৯৫৮ সালে John McCarthy-র Lisp ভাষায় LAMBDA শব্দটি প্রোগ্রামিংয়ে ঢোকে। C++-এ lambda এসেছে অনেক পরে — C++11-এ, ২০১১ সালে। তার আগে প্রায় এক দশক ধরে C++ প্রোগ্রামাররা Boost.Lambda-র মতো লাইব্রেরি দিয়ে template-এর কৌশলে এর অনুকরণ করতেন।

পরীক্ষা ও ইন্টারভিউতে
  • lambda আর closure-এর পার্থক্য কী? — lambda হলো কোডে লেখা expression; closure হলো চলার সময় তা থেকে তৈরি object, যার class compiler বানায়।
  • [x] আর [&x]-এর পার্থক্য? — প্রথমটি তৈরির মুহূর্তের কপি রাখে; দ্বিতীয়টি আসল ভ্যারিয়েবলের reference, তাই পরের পরিবর্তন দেখে, কিন্তু ভ্যারিয়েবল মরে গেলে dangling.
  • mutable কেন লাগে? — closure-এর operator() ডিফল্টে const, তাই value-capture করা কপি বদলাতে mutable লিখতে হয়; এটি বাইরের ভ্যারিয়েবল বদলায় না।
  • কোন lambda function pointer-এ রূপান্তরিত হয়? — শুধু যা কিছুই capture করে না, কারণ তার কোনো state নেই।
  • std::function না auto? — local বা template-এ auto (খরচ নেই); নানা টাইপের callable এক টাইপে রাখতে হলে std::function (পরোক্ষ ডাক, সম্ভাব্য heap, খালি হতে পারে)।

৯ · AI-এর যুগে — lambda-র ভুল ধরা

AI টুল lambda লিখতে খুব পছন্দ করে — কোড ছোট দেখায়, আধুনিক দেখায়। আর বেশিরভাগ সময় কোডটি compile হয় এবং ছোট পরীক্ষায় ঠিকঠাক চলে। সমস্যা হলো, lambda-র সবচেয়ে বড় ভুল — dangling capture — ঠিক এমন যে ছোট পরীক্ষায় ধরা পড়ে না। callback পরে চলে, thread পরে চলে, আর তখন মৃত মেমরিতে যা ছিল তা হয়তো এখনো আছে। নিচের প্রবণতাগুলো চোখে রাখুন:

AI যা করেসমস্যাযা হওয়া উচিত
সব জায়গায় [&], এমনকি callback, thread বা ফেরত দেওয়া lambda-তেlambda ভ্যারিয়েবলের চেয়ে বেশি দিন বাঁচলে dangling — UB, প্রায়ই পরীক্ষায় ধরা পড়ে নাসংরক্ষিত lambda-তে নাম ধরে value বা init-capture
member function-এ [=] বা [this] দিয়ে async কাজশুধু this pointer কপি হয়; object আগে মরলে dangling[*this], বা প্রয়োজনীয় সদস্যগুলো আলাদা কপি; অথবা shared_ptr
প্রতিটি callable-কে std::function বানানো, এমনকি local ভ্যারিয়েবল আর ফাংশনের প্যারামিটারেওঅকারণে পরোক্ষ ডাক আর সম্ভাব্য heap; inline হয় নাauto, বা template <class F>; std::function শুধু যেখানে টাইপ মুছতেই হবে
২০-৩০ লাইনের lambda, ভেতরে আরেকটি lambdaপড়া, পরীক্ষা করা আর পুনর্ব্যবহার কঠিন; stack trace-এ নামহীননাম দেওয়া ফাংশন বা class; lambda থাকুক এক-দুই লাইনে
comparator-এ <=sort-এর শর্ত ভাঙে — UB: ভুল ক্রম, এমনকি crashকঠোর <, বা std::tie(...) < std::tie(...)
নতুন কোডে std::bindপড়তে কঠিন, overload-এ বিভ্রান্তিlambda

AI-কে callback বা lambda-ভারী কোড লিখতে বলার সময় capture-নীতি আগেই বলে দিন, আর প্রতিটি capture-এর পক্ষে lifetime-এর যুক্তি চান:

আমি C++23 শিখছি (g++, -Wall -Wextra). একটি ছোট event system লেখো: একটি Button class, যার onClick একটি callback-এর তালিকা (std::function<void()>), আর click() সব callback চালায়। তিনটি callback যোগ করো: কার্টের সংখ্যা বাড়ানো, একটি বার্তা ছাপানো, আর একটি tag সহ গণনা। নিয়ম: ১. কোথাও [&] বা [=] লিখবে না — প্রতিটি capture নাম ধরে লিখবে। ২. যে lambda তালিকায় সংরক্ষিত হয়, সে কোনো local ভ্যারিয়েবল reference-এ ধরবে না; দরকার হলে value, init-capture বা shared_ptr. ৩. প্রতিটি capture-এর পাশে একটি comment দাও: ধরা জিনিসটি lambda-র চেয়ে বেশি দিন বাঁচে কেন। ৪. শুধু callback তালিকায় std::function; বাকি জায়গায় auto বা template. ৫. কোনো lambda তিন লাইনের বেশি হলে সেটিকে নাম দেওয়া ফাংশন বানাও। কোডের পরে বলো: Button object যদি কার্টের আগে ধ্বংস হয়, বা উল্টোটা, কোন callback বিপদে পড়বে।

AI-এর lambda যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১“lambda” নামটি এসেছে ১৯৩০-এর দশকের একটি গাণিতিক ব্যবস্থা থেকে, কম্পিউটার তৈরিরও আগে। Alonzo Church-এর lambda calculus কী ছিল, আর তা কীভাবে আজকের প্রোগ্রামিং ভাষায় পৌঁছাল?

১৯৩০-এর দশকের শুরুতে Princeton-এ গণিতবিদ Alonzo Church গণিতের ভিত্তি নিয়ে কাজ করছিলেন। তখনকার একটি বড় প্রশ্ন ছিল: “হিসাব করা যায়” বলতে ঠিক কী বোঝায়? কোন সমস্যার সমাধান একটি যান্ত্রিক নিয়মে পৌঁছানো যায়, আর কোনটির যায় না? এর উত্তর খুঁজতে Church তৈরি করেন lambda calculus — একটি আশ্চর্য সরল ব্যবস্থা, যেখানে আছে শুধু তিনটি জিনিস: ভ্যারিয়েবল, ফাংশন তৈরি (λx. … লিখে — “x নেয় এমন একটি ফাংশন”), আর ফাংশন প্রয়োগ। সংখ্যা নেই, if নেই, লুপ নেই — অথচ দেখা গেল এগুলো সবই শুধু নাম-ছাড়া ফাংশন দিয়ে তৈরি করা যায়। “λx. x + 1” আসলে সেই জিনিস, যা আজ C++-এ লেখা হয় [](int x) { return x + 1; }.

১৯৩৬ সালের দিকে Church এই ব্যবস্থা দিয়ে দেখান যে এমন কিছু প্রশ্ন আছে, যার উত্তর কোনো যান্ত্রিক পদ্ধতি দিতে পারে না। প্রায় একই সময়ে Alan Turing আলাদা পথে — একটি কাল্পনিক যন্ত্র, Turing machine দিয়ে — একই ধরনের ফলাফলে পৌঁছান। পরে প্রমাণ হয়, দুটি মডেল সমান শক্তিশালী: যা একটিতে হিসাব করা যায়, অন্যটিতেও যায়। এই ধারণাই Church–Turing thesis নামে পরিচিত। Turing পরে Princeton-এ Church-এর অধীনেই PhD করেন। মজার বিষয়, “কেন λ অক্ষর” — এ নিয়ে নানা গল্প চালু আছে, আর Church নিজেও এর তেমন গভীর কারণ দেননি।

প্রোগ্রামিংয়ে ধারণাটি আসে ১৯৫৮ সালে, যখন MIT-তে John McCarthy Lisp তৈরি করেন আর ফাংশন লেখার জন্য সরাসরি LAMBDA শব্দটি নেন। সেখান থেকে Scheme, ML, Haskell-এর মতো functional ভাষায় নাম-ছাড়া ফাংশন হয়ে ওঠে মূল যন্ত্র। পরে প্রায় সব মূলধারার ভাষায় এটি ঢোকে — Python-এর lambda, JavaScript-এর ফাংশন expression আর arrow function, C# 3.0 (২০০৭), C++11 (২০১১), Java 8 (২০১৪)।

একটি গুরুত্বপূর্ণ পার্থক্য খেয়াল করুন। lambda calculus-এ “ভ্যারিয়েবল ধরে রাখা” নিয়ে কোনো চিন্তা নেই — সেখানে মেমরি নেই, lifetime নেই। C++ সেই গাণিতিক ধারণাকে বাস্তব মেশিনে নামিয়ে এনেছে, তাই তাকে প্রশ্ন করতে হয়েছে: ধরা মানটি কপি হবে, নাকি reference? কে কতক্ষণ বাঁচবে? capture list হলো গণিতের বিশুদ্ধ ধারণার সাথে মেশিনের বাস্তবতার মিলনবিন্দু।

মূল উপলব্ধি: আপনি যখন একটি lambda লেখেন, তখন আপনি প্রায় নব্বই বছরের পুরোনো একটি গাণিতিক ধারণা ব্যবহার করছেন — যা মূলত তৈরি হয়েছিল “হিসাব কী” প্রশ্নের উত্তর খুঁজতে। “ফাংশনও একটি মান, যা তৈরি করা, পাঠানো আর ফেরত দেওয়া যায়” — এই ভাবনাটিই lambda-র আসল শক্তি।

প্র ০২C++-এ lambda এল মাত্র C++11-এ। তার আগে প্রোগ্রামাররা Boost.Lambda-র মতো লাইব্রেরি দিয়ে এর অনুকরণ করতেন। সেটি কীভাবে কাজ করত, আর কেন তা এত কষ্টকর ছিল যে শেষে ভাষাতেই lambda আনতে হলো?

১৯৯৮ সালের প্রথম C++ standard-এ STL algorithm ছিল, কিন্তু lambda ছিল না। ফলে std::for_each বা std::sort-এ নিয়ম দিতে হলে লিখতে হতো একটি পুরো functor class — সাধারণত ফাইলের ওপরে, ব্যবহারের জায়গা থেকে দূরে। আর C++98-এ local class (ফাংশনের ভেতরে ঘোষিত class) template argument হিসেবে ব্যবহার করা যেত না, তাই সেই class ফাংশনের ভেতরেও লেখা যেত না। অনেকে এই ঝামেলার চেয়ে হাতে লেখা for লুপকেই সহজ মনে করতেন, আর algorithm-এর সুবিধা অব্যবহৃত থেকে যেত।

এই ফাঁক পূরণ করতে Jaakko Järvi আর Gary Powell তৈরি করেন Boost.Lambda লাইব্রেরি (২০০০-এর দশকের শুরুতে Boost-এ যোগ হয়)। এর কৌশল ছিল চমকপ্রদ: _1, _2 নামের বিশেষ “placeholder” object, যাদের ওপর সব operator overload করা। std::for_each(v.begin(), v.end(), std::cout << _1 << ' '); — এখানে std::cout << _1 কিছু ছাপায় না, বরং একটি object বানায় যা “পরে কেউ একটি মান দিলে সেটি ছাপাবে” — একে বলে expression template. অর্থাৎ পুরো expression-টি একটি function object-এ রূপ নেয়।

কিন্তু সীমাগুলো ছিল কঠিন। শুধু operator দিয়ে লেখা যায় এমন জিনিসই সহজে লেখা যেত; একটি সাধারণ member function ডাকতেই লাগত bind-এর জটিল রূপ। if বা লুপের জন্য আলাদা অদ্ভুত গঠন (if_then, for_loop) শিখতে হতো। একটি ভুল হলে compiler যে error message দিত, তা প্রায়ই শত শত লাইনের template-এর জঙ্গল — আসল ভুলটা খুঁজে পাওয়াই কঠিন। compile-এর সময় অনেক বেড়ে যেত। আর কোডটি এমন দেখাত যে সাধারণ C++ প্রোগ্রামার পড়ে বুঝতেন না। পরে Boost.Phoenix আরও শক্তিশালী সংস্করণ আনে, কিন্তু মূল সমস্যা — ভাষার বাইরে থেকে ভাষার একটি মৌলিক অংশ বানানো — থেকেই যায়।

এই অভিজ্ঞতা থেকে Järvi নিজেই, John Freeman ও Lawrence Crowl-এর সাথে, C++ কমিটিতে সরাসরি ভাষায় lambda আনার প্রস্তাব দেন। তখন C++0x নামে নতুন standard তৈরি হচ্ছিল, যা শেষে ২০১১ সালে C++11 হিসেবে আসে। নকশায় একটি গুরুত্বপূর্ণ সিদ্ধান্ত ছিল capture list: অন্য ভাষার মতো সবকিছু নিঃশব্দে ধরে নেওয়ার বদলে প্রোগ্রামারকে বলতে হবে কী কপি হবে আর কী reference. আর ভেতরে lambda সাধারণ একটি function object-ই, তাই এর খরচ হাতে লেখা functor-এর সমান — zero-overhead নীতির সাথে পুরো মিল।

মূল উপলব্ধি: লাইব্রেরি দিয়ে ভাষার ঘাটতি অনেকদূর ঢাকা যায় — Boost.Lambda তার প্রমাণ — কিন্তু তার দাম দেয় পাঠযোগ্যতা, error message আর compile সময়। কোনো কৌশল যখন সবাই বারবার কষ্ট করে বানায়, তখনই সেটি ভাষায় ঢোকার সময়।

প্র ০৩std::function যেকোনো callable রাখতে পারে — কিন্তু কীভাবে, যখন প্রতিটি closure-এর মাপ আলাদা? small buffer optimization কী, আর std::function-এর আসল খরচ কোথায়?

সমস্যাটা আগে বুঝুন। একটি std::function<int(int)>-এর মাপ নির্দিষ্ট — sizeof একটি স্থির সংখ্যা। অথচ এতে রাখা যায় একটি capture-ছাড়া lambda (মাপ প্রায় শূন্য), একটি int capture করা lambda, বা পুরো একটি vector capture করা lambda (অনেক বড়)। স্থির মাপের বাক্সে বদলানো মাপের জিনিস রাখার প্রচলিত উপায় হলো: জিনিসটি heap-এ রাখা, আর বাক্সে রাখা তার pointer. আর ডাকার জন্য বাক্স রাখে আরেকটি ছোট “ব্যবস্থাপক” — একটি ফাংশন pointer বা virtual ফাংশনের মতো কিছু — যা জানে ভেতরের আসল টাইপটিকে কীভাবে ডাকতে, কপি করতে আর ধ্বংস করতে হয়। এটাই type erasure: বাইরে একটি টাইপ, ভেতরে যেকোনো টাইপ।

কিন্তু প্রতিটি ছোট lambda-র জন্য heap থেকে মেমরি নেওয়া ধীর — new আর delete প্রতিবার কিছুটা সময় নেয়, আর মেমরি ছড়িয়ে পড়ে। তাই বাস্তবায়নগুলো একটি কৌশল নেয়: std::function-এর নিজের ভেতরে কয়েকটি pointer-এর সমান একটি ছোট জায়গা রাখা। callable সেখানে আঁটলে heap-এ যাওয়ার দরকার নেই। একে বলে small buffer optimization (SBO), পর্ব ১৪-এর std::string-এর SSO-র মতোই ধারণা। standard শুধু বলে যে ছোট callable — যেমন function pointer — এর জন্য বাস্তবায়ন heap এড়ানোর চেষ্টা করবে; বাফারের আসল মাপ বাস্তবায়নের ওপর নির্ভর করে, আর GCC, Clang ও MSVC-র লাইব্রেরিতে মাপ আলাদা। ফলে একই কোড এক compiler-এ heap ব্যবহার করে, অন্যটিতে করে না।

আসল খরচ তাহলে কোথায়? প্রথমত, প্রতিটি ডাক পরোক্ষ — বাক্সকে ব্যবস্থাপকের মাধ্যমে আসল ফাংশনে যেতে হয়। দ্বিতীয়ত, এবং সবচেয়ে বড়: compiler সাধারণত ভেতরে কী আছে জানে না, তাই inline করতে পারে না। একটি sort-এ comparator লাখবার ডাকা হয়; auto বা template-এ সেটি inline হয়ে কয়েকটি মেশিন instruction-এ নেমে আসে, std::function-এ প্রতিবার একটি পূর্ণ ডাক। তৃতীয়ত, বড় capture-এ heap allocation. চতুর্থত, std::function কপি করা যায়, তাই ভেতরের callable-ও কপি করা যায় এমন হতে হয় — এটি move-only জিনিস রাখার পথ বন্ধ করে।

এ কারণেই standard library পরে আরও বিশেষায়িত যন্ত্র এনেছে: C++23-এ std::move_only_function (কপির শর্ত নেই), আর C++26-এর জন্য গৃহীত std::function_ref — যা কিছুর মালিক হয় না, শুধু একটি callable-কে “দেখে”, তাই কখনো heap লাগে না; ফাংশনের প্যারামিটার হিসেবে callback নিতে এটি আদর্শ। কোনটি কখন, তা নির্ভর করে মালিকানা আর lifetime-এর ওপর।

মূল উপলব্ধি: std::function-এর নমনীয়তা বিনা মূল্যে আসে না — দাম হলো পরোক্ষ ডাক, inline হারানো আর সম্ভাব্য heap. তাই এটি ব্যবহার করুন যেখানে সত্যিই নানা টাইপকে এক টাইপে মুছতে হবে; বাকি জায়গায় auto আর template আপনাকে সেই দাম ছাড়াই একই কাজ দেয়।

প্র ০৪capture-ছাড়া lambda function pointer-এ রূপান্তরিত হয়, কিন্তু capture-সহ lambda হয় না। এর পেছনের যুক্তি কী — আর C লাইব্রেরি callback-এর সাথে কোনো তথ্য পাঠানোর সমস্যা কীভাবে সমাধান করে?

একটি function pointer আসলে শুধু একটি সংখ্যা — মেশিন কোডের কোথায় একটি ফাংশন শুরু হয়েছে, তার ঠিকানা। এতে কোনো ডেটা রাখার জায়গা নেই। অংশ ৩-এ দেখেছেন, একটি closure হলো একটি object: তার class-এর operator() একটি member function, যাকে ডাকতে object-টি (this) লাগে, কারণ capture করা মানগুলো সেই object-এর ভেতরে। capture না থাকলে object-এ কিছুই নেই — operator() কখনো this ব্যবহারই করে না। তখন compiler সহজেই একটি সাধারণ, স্বাধীন ফাংশন বানাতে পারে যা একই কাজ করে, আর তার ঠিকানা দিতে পারে। standard তাই প্রতিটি capture-ছাড়া closure টাইপে একটি conversion function রাখে, যা এমন একটি function pointer দেয় (C++17 থেকে এটি constexpr-ও)। আর একটি ছোট কৌশল: lambda-র আগে unary + লিখলে (+[](int x) { ... }) রূপান্তরটি জোর করে ঘটানো যায়, যা কখনো template-এর টাইপ নির্ণয়ে কাজে লাগে।

capture-সহ lambda-র ক্ষেত্রে এই কৌশল চলে না: [k](int a) { return a + k; }-এর প্রতিটি closure-এ k-এর আলাদা মান থাকতে পারে, কিন্তু function pointer একটিই ঠিকানা — সেটি কোন k ব্যবহার করবে? ডেটা কোথাও না কোথাও রাখতে হবে, আর function pointer-এ সেই জায়গা নেই।

C লাইব্রেরিগুলো বহু দশক আগেই এই সমস্যার মুখোমুখি হয়েছে, আর তাদের প্রচলিত সমাধান হলো callback-এর সাথে একটি বাড়তি void* — প্রায়ই নাম user data বা context. যেমন POSIX-এর pthread_create start ফাংশনের সাথে একটি void* arg নেয়; অনেক GUI আর নেটওয়ার্ক লাইব্রেরিতেও একই নকশা। আপনি নিজের ডেটার ঠিকানা সেখানে দেন, আর লাইব্রেরি callback ডাকার সময় সেটি ফেরত দেয়। C++ থেকে এটি ব্যবহারের একটি পরিচিত কৌশল: closure object-এর ঠিকানাই void* হিসেবে পাঠানো, আর callback হিসেবে একটি capture-ছাড়া lambda দেওয়া, যা void*-কে আবার আসল closure টাইপে cast করে ডাকে। মজার বিষয়, standard C-এর qsort-এ এই context প্যারামিটার নেই — যে কারণে C11-এর ঐচ্ছিক Annex K-এ qsort_s, আর কিছু প্ল্যাটফর্মে qsort_r আছে, যদিও প্ল্যাটফর্মভেদে এদের প্যারামিটারের ক্রম পর্যন্ত আলাদা।

এই নকশার ঝুঁকিও চোখে রাখুন: void* কোনো টাইপ যাচাই করে না, আর closure-টি callback চলার সময় বেঁচে আছে কি না, তা নিশ্চিত করা আপনার দায়িত্ব — আবারও সেই lifetime-এর প্রশ্ন।

মূল উপলব্ধি: নিয়মটি মুখস্থের নয়, যুক্তির: state না থাকলে একটি object-এর দরকার নেই, তাই শুধু কোডের ঠিকানাই যথেষ্ট। আর state থাকলে তাকে কোথাও রাখতেই হবে — C-তে সেটি void*-এ, C++-এ closure object-এ।

প্র ০৫JavaScript বা Python-এ closure কোনো capture list ছাড়াই বাইরের ভ্যারিয়েবল ধরে ফেলে। তাহলে C++ কেন প্রোগ্রামারকে [x] বা [&x] লিখতে বাধ্য করে? কোন নকশাটি ভালো?

JavaScript-এ একটি ফাংশন বাইরের যেকোনো ভ্যারিয়েবল ব্যবহার করলে সেটি স্বয়ংক্রিয়ভাবে ধরা পড়ে — কপি হিসেবে নয়, বরং ভ্যারিয়েবলটির সাথেই একটি জীবন্ত সংযোগ হিসেবে। এটি নিরাপদ, কারণ সেখানে garbage collector আছে: যতক্ষণ কোনো closure ভ্যারিয়েবলটি ব্যবহার করতে পারে, ততক্ষণ সেটি মুছে ফেলা হয় না। ফাংশন শেষ হলেও ভ্যারিয়েবল বাঁচে, closure-এর দরকার যতদিন। Python-ও প্রায় একইভাবে কাজ করে। ফলে এসব ভাষায় dangling capture বলে কিছু নেই।

কিন্তু জীবন্ত সংযোগেরও নিজস্ব ফাঁদ আছে। Python-এর বিখ্যাত উদাহরণ: fs = [lambda: i for i in range(3)] — তারপর প্রতিটি ডাকলে আসে 2, 2, 2; কারণ তিনটি lambda-ই একই i দেখে, যার শেষ মান 2 (একে “late binding” বলে)। সমাধান হিসেবে Python প্রোগ্রামাররা লেখেন lambda i=i: i — অর্থাৎ হাতে কপি নেওয়া, যা C++-এর [i]-এর সমান। JavaScript-এ পুরোনো var দিয়ে লুপে একই সমস্যা হতো; ২০১৫ সালের ES2015-এ let আসার পর প্রতিটি পাকে আলাদা ভ্যারিয়েবল তৈরি হয়। অর্থাৎ যেসব ভাষা “ভাবতে হয় না” বলে শুরু করেছিল, তাদেরও শেষে কপি আর reference-এর পার্থক্য শেখাতে হয়েছে।

C++-এ garbage collector নেই। একটি local ভ্যারিয়েবল তার scope শেষ হলেই মরে — এটাই RAII আর C++-এর গতির ভিত্তি। তাই C++ নিঃশব্দে “জীবন্ত সংযোগ” দিতে পারে না, কারণ সেই সংযোগ ভ্যারিয়েবলকে বাঁচিয়ে রাখতে পারবে না। আবার নিঃশব্দে সব কপি করাও ঝুঁকিপূর্ণ — একটি বড় vector হঠাৎ কপি হলে খরচ লুকিয়ে যায়, আর বদলানোর ইচ্ছা থাকলে ভুল ফল আসে। তাই C++ সিদ্ধান্তটা প্রোগ্রামারের হাতে দিয়েছে, আর লিখে রাখতে বলেছে। [&] আর [=] হলো সুবিধার ছাড়, কিন্তু সিদ্ধান্তের দায় তবু আপনার।

অন্য ভাষাগুলোও এই প্রশ্নের নিজের নিজের উত্তর খুঁজেছে। Java 8 lambda-তে শুধু “effectively final” — অর্থাৎ কখনো বদলায় না এমন — local ভ্যারিয়েবল ধরতে দেয়, তাই কপি আর reference-এর পার্থক্যই মুছে যায়। Swift-এ ঐচ্ছিক capture list আছে, যেমন [weak self], যা reference cycle ঠেকাতে ব্যবহার হয়। Rust সবচেয়ে কাছের: closure ডিফল্টে ধার (borrow) নেয়, আর move লিখলে মালিকানা নিয়ে যায় — কিন্তু পার্থক্য হলো Rust-এর compiler (borrow checker) প্রমাণ করে যে ধার নেওয়া জিনিস closure-এর আগে মরছে না; ভুল হলে compile-ই হয় না। C++ সেই প্রমাণের কাজটি এখনো প্রোগ্রামার আর sanitizer-এর হাতে রেখেছে।

মূল উপলব্ধি: capture list কোনো বাড়তি ঝামেলা নয় — এটি garbage collector না থাকার স্বাভাবিক ফল। যে ভাষা lifetime নিজে সামলায় না, সেখানে প্রতিটি ধরা ভ্যারিয়েবলের lifetime কোডে স্পষ্ট লেখা থাকাই সবচেয়ে সৎ নকশা।

অনুশীলন

বেশিরভাগ কাজে কোড লিখতে হবে না — আগে অনুমান করুন, তারপর উত্তর মিলিয়ে নিন।

  1. আউটপুট অনুমান: int x = 1; তারপর auto a = [x] { return x; }; আর auto b = [&x] { return x; };. এরপর x = 5; আর auto c = [x]() mutable { return ++x; };. শেষে a(), b(), c(), c() আর x — কী কী মান?

    a() = 1 (তৈরির মুহূর্তের কপি)। b() = 5 (reference — বর্তমান মান)। c তৈরি হয়েছে x = 5 হওয়ার পরে, তাই তার কপি 5; প্রথম c() = 6, দ্বিতীয় c() = 7 (mutable — নিজের কপি বাড়ছে)। আর x এখনো 5, কারণ c বাইরের x ছোঁয় না।

  2. বাগ শিকার: একটি AI এই কোড দিয়েছে — auto make_greeter() { std::string name = "Rahim"; return [&name] { return "Hi " + name; }; }, আর পরে auto g = make_greeter(); std::println("{}", g());. কোথায় সমস্যা, আর দুটি সমাধান কী?

    lambda name-কে reference-এ ধরেছে, কিন্তু name হলো make_greeter-এর local — ফাংশন ফেরত দিতেই সেটি ধ্বংস। g() একটি মৃত string পড়ে: undefined behavior (কখনো ঠিক দেখাতে পারে, যা আরও বিপজ্জনক)। সমাধান ১: [name] — কপি। সমাধান ২: [name = std::move(name)] — কপি ছাড়াই string-টি closure-এ সরিয়ে নেওয়া (init-capture, C++14). ফেরত দেওয়া lambda-তে reference capture প্রায় সবসময় ভুল।

  3. নকশা ভাবুন: প্রতিটি ক্ষেত্রে callable রাখার জন্য কোনটি বেছে নেবেন — auto/template, std::function, নাকি function pointer: (ক) একটি ফাংশনের ভেতরে একবার ব্যবহৃত comparator (খ) একটি Timer class, যা ব্যবহারকারীর দেওয়া যেকোনো কাজ সদস্য হিসেবে রেখে পরে চালাবে (গ) C-তে লেখা একটি লাইব্রেরির register_handler(void (*)(int))-এ callback দেওয়া (ঘ) retry(f, 3) ধরনের একটি সাধারণ utility, যা খুব ঘন লুপে ডাকা হয়।

    (ক) auto — local, একটিই টাইপ, inline হবে। (খ) std::function<void()> — সদস্যের টাইপ আগে থেকে লিখতে হবে, আর ব্যবহারকারী নানা ধরনের lambda দেবেন; এখানেই type erasure-এর সত্যিকারের দরকার। (গ) function pointer — C লাইব্রেরি শুধু এটাই চেনে; capture-ছাড়া lambda দিন, তথ্য লাগলে লাইব্রেরির context প্যারামিটার (থাকলে) ব্যবহার করুন। (ঘ) template প্যারামিটার (template <class F> void retry(F f, int n)) — প্রতিটি callable-এর জন্য আলাদা সংস্করণ, inline হয়, ঘন লুপে std::function-এর পরোক্ষ ডাকের খরচ নেই।

  4. নিজে লিখুন (কোড): উপরের shop_lambdas.cpp বদলান: পণ্যগুলো এবার বেশি দাম আগে সাজান, দাম সমান হলে নাম অনুযায়ী (A থেকে Z). ছাড় করুন ২৫%. আর গুনুন কয়টি পণ্যের ছাড়ের পরের দাম ২০০ বা তার বেশি — ছাড়ের lambda-টি count_if-এর lambda-র ভেতরে ব্যবহার করে। আউটপুট আগে অনুমান করুন।

    comparator: if (a.price != b.price) return a.price > b.price; return a.name < b.name;. ছাড়: auto sale = make_discount(25);. গোনা: std::ranges::count_if(items, [&sale](const Product& p) { return sale(p.price) >= 200; }) — এখানে [&sale] নিরাপদ, কারণ lambda সঙ্গে সঙ্গে চলে শেষ (চাইলে [sale]-ও ঠিক)। আউটপুট: bag: 900 -> 675, cap: 250 -> 188, ink: 20 -> 15, pen: 20 -> 15, তারপর গণনা 1 (শুধু bag; cap-এর 188 < 200). লক্ষ করুন, ২৫০-এর ২৫% = ৬২.৫, কিন্তু পূর্ণসংখ্যার ভাগে 62, তাই 188.

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমC++-এ auto pass = [limit](int m) { return m >= limit; }; লেখা থেকে pass(45) চলা পর্যন্ত ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. compiler এই lambda-র জন্য একটি নাম-ছাড়া closure class বানায়
  2. capture করা limit সেই class-এর একটি data member হয়
  3. lambda-র body class-এর একটি const operator() হয়
  4. চলার সময় closure object তৈরি হয়, আর limit-এর মান তাতে কপি হয়
  5. pass(45) সেই object-এর operator() ডাকে

C++-এ int x = 10; auto f = [x] { return x; }; x = 20; — এরপর f() কী ফেরত দেয়?

  • 10
  • 20
  • Compile error
  • Undefined behavior

C++-এ [count] { ++count; } compile হয় না। কেন, আর সমাধান কী?

  • value capture নিষিদ্ধ; [&count] লিখতেই হবে
  • lambda-তে ++ চলে না; count + 1 ফেরত দিতে হবে
  • operator() ডিফল্টে const; mutable লিখতে হবে
  • ফেরত-টাইপ -> void লিখতে হবে

C++-এ নিচের কোন lambda নিজে থেকে int (*)(int) function pointer-এ রূপান্তরিত হতে পারে?

  • [k](int a) { return a + k; }
  • [](int a) { return a * 2; }
  • [&](int a) { return a + k; }
  • [n = 5](int a) { return a + n; }

C++-এ একটি খালি std::function<void()> (কিছু রাখা হয়নি) ডাকলে কী হয়?

  • undefined behavior
  • কিছুই হয় না, চুপচাপ ফেরত আসে
  • compile error
  • std::bad_function_call exception ছোড়া হয়

C++-এ [v = std::move(v)] ধরনের init-capture কোন standard থেকে লেখা যায়?

  • C++98
  • C++11
  • C++14
  • C++20

একটি C++ ফাংশন নিজের local ভ্যারিয়েবলকে [&] দিয়ে ধরা একটি lambda একটি global callback তালিকায় রেখে ফেরত দিল। পরে callback চালালে কী হয়?

  • ভ্যারিয়েবলটি callback-এর জন্য বাঁচিয়ে রাখা হয়, সব ঠিক
  • dangling reference — undefined behavior
  • compile error
  • lambda নিজে থেকে ভ্যারিয়েবলের কপি নিয়ে নেয়

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. lambda = নাম-ছাড়া ছোট ফাংশন, দরকারের জায়গাতেই লেখা; ভেতরে compiler-এর বানানো একটি closure class, যার const operator() আছে।
  2. [x] কপি, [&x] জীবন্ত সংযোগ; কপি বদলাতে mutable, নতুন বা সরিয়ে আনা মানে init-capture (C++14), object-এর কপিতে [*this] (C++17).
  3. সংরক্ষিত, ফেরত দেওয়া বা অন্য thread-এর lambda-তে reference capture নয় — lambda ভ্যারিয়েবলের চেয়ে বেশি দিন বাঁচলে dangling, undefined behavior.
  4. রাখতে auto বা template আগে; নানা টাইপ এক টাইপে লাগলে std::function (খরচ আছে, খালি হতে পারে); C API-তে capture-ছাড়া lambda = function pointer.
  5. ছোট রাখুন: generic lambda-য় auto প্যারামিটার, factory-তে lambda ফেরত, const মানে IIFE, std::bind-এর বদলে lambda — আর বড় নিয়ম হলে নাম দেওয়া ফাংশন।

পরের পর্বে: lambda হাতে এলে C++20-এর ranges আর views খুলে যায় — marks | views::filter(...) | views::transform(...) ধরনের পাইপলাইন, যেখানে ডেটা কপি না করেই ধাপে ধাপে ছাঁকা আর বদলানো যায়।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান