সরাসরি পর্বে যান
পর্ব ০৯ · ৫৫-এর মধ্যে · মডিউল ২

ফাংশন ও overloading — এক নাম, অনেক কাজ

Functions & Overloading — Reusable Promises in C++

Taught in Bangla, this lesson covers functions in C++: pass-by-value, the call stack, default arguments, function overloading and how the compiler resolves overloads, recursion, and modern features such as auto return, [[nodiscard]], inline and constexpr.

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

  • ফাংশনের গঠন — return টাইপ, নাম, parameter, শরীর — আর declaration ও definition-এর পার্থক্য
  • Pass-by-value আসলে কী কপি করে, আর call stack-এ ফাংশন-ডাক কীভাবে জমে ও মুছে যায়
  • Default argument-এর নিয়ম, আর কখন এটা সুবিধার বদলে বিভ্রান্তি তৈরি করে
  • Function overloading, আর compiler কীভাবে সঠিক overload বেছে নেয় (বা বেছে নিতে ব্যর্থ হয়)
  • আধুনিক ফাংশন-লেখা: auto return, trailing return, [[nodiscard]], inline-এর আজকের অর্থ আর recursion

১ · ফাংশন — নাম দেওয়া, বারবার ব্যবহারযোগ্য একটি প্রতিশ্রুতি

আগের পর্বে আপনি if, switch আর লুপ দিয়ে সিদ্ধান্ত নিতে ও কাজ বারবার করাতে শিখেছেন। কিন্তু ধরুন একটি দোকানের সফটওয়্যারে VAT-সহ দাম হিসাব লাগে বিশ জায়গায়। বিশ জায়গায় একই তিন লাইন কপি করলে কী হয়? হার বদলালে বিশ জায়গায় বদলাতে হয়, একটি ভুলে গেলেই হিসাব গড়বড়। সমাধান — কাজটিকে একবার লিখে একটি নাম দেওয়া, তারপর যেখানে দরকার সেখানে নাম ধরে ডাকা। এই নামওয়ালা কাজের টুকরোর নাম function.

পাড়ার চায়ের দোকানে গিয়ে আপনি বলেন, “মামা, দুধ-চিনি কম দিয়ে একটা চা।” আপনি জানেন না কেতলিতে পানি কতক্ষণ ফুটছে, পাতা কোন ব্র্যান্ডের। আপনি শুধু জানেন একটি চুক্তি: এই চাহিদা (ইনপুট) দিলে এক কাপ চা (আউটপুট) পাবেন। ফাংশন ঠিক এমন একটি চুক্তি — ডাকার মানুষ জানে কী দিতে হবে আর কী পাবে; কীভাবে হয়, সেটা ফাংশনের নিজের ব্যাপার।

একটি ফাংশনের চুক্তি

Inputs go in, one result comes out

  1. Argument500, 2ডাকার সময় দেওয়া মান
  2. ফাংশনbill(price, qty)ভেতরের কাজ লুকানো
  3. Return value1150.0একটি নির্দিষ্ট টাইপের ফল
ছবি ৯.১ — ডাকার মানুষ শুধু চুক্তিটা দেখে: কী দিতে হবে, কী পাওয়া যাবে।

ফাংশনের চারটি অংশ

double bill(double price, int qty) {   // return টাইপ, নাম, parameter-তালিকা
    double sub = price * qty;           // শরীর (body)
    return sub * 1.15;                  // ফল ফেরত
}

double t = bill(500, 2);                // ডাক: 500 আর 2 হলো argument

📤 Return টাইপ

নামের আগে লেখা — ফল কোন টাইপের হবে। কোনো ফল না দিলে void, যেমন শুধু স্ক্রিনে কিছু ছাপানোর ফাংশন।

কী ফেরত আসবে

🏷️ নাম

কাজটা কী, এক নজরে বোঝায় — bill, area, isPassed. ভালো নাম অর্ধেক documentation.

কাজের পরিচয়

📥 Parameter

ব্র্যাকেটের ভেতরে ইনপুটের টাইপ আর নাম: double price, int qty. প্রতিটি parameter ফাংশনের ভেতরে একটি সাধারণ ভ্যারিয়েবল।

কী দিতে হবে

🧱 শরীর

{ }-এর ভেতরের আসল কাজ। return চলামাত্র ফাংশন শেষ, ফল চলে যায় ডাকার জায়গায় — এটাই return value.

কীভাবে হবে

দুটি শব্দ প্রায়ই গুলিয়ে যায়। সংজ্ঞায় লেখা price আর qty হলো parameter — খালি ঘরের নাম। ডাকার সময় যে আসল মান পাঠান, যেমন 500 আর 2, সেগুলো argument. ফর্মের ঘর “নাম: ______” হলো parameter, আপনি যা লিখে দিলেন তা argument.

Declaration বনাম definition — প্রতিশ্রুতি আর কাজ

C++ compiler ফাইল ওপর থেকে নিচে পড়ে। কোনো নাম ব্যবহারের আগে সে জানতে চায় নামটি কী — কয়টি parameter, কোন টাইপ, কী ফেরত দেয়। তাই ফাংশন দুই ভাগে লেখা যায়:

double bill(double price, int qty);    // declaration: শুধু প্রতিশ্রুতি, শেষে ;

int main() {
    double t = bill(500, 2);            // compiler প্রতিশ্রুতি দেখেই খুশি
}

double bill(double price, int qty) {   // definition: আসল কাজ
    return price * qty * 1.15;
}

প্রথম লাইনটি declaration (একে prototype-ও বলে) — শরীর নেই, শুধু চুক্তি। নিচেরটি definition — চুক্তির সাথে আসল কাজ। একটি ফাংশন যতবার খুশি declare করা যায়, কিন্তু পুরো প্রোগ্রামে definition থাকবে একবারই। Compiler declaration দেখে ডাকটা ঠিক আছে কি না যাচাই করে; definition খুঁজে ডাকের সাথে জোড়া লাগায় linker (পর্ব ০৩-এর “undefined reference” error মনে আছে?)। বড় প্রোগ্রামে declaration থাকে header ফাইলে, definition থাকে .cpp ফাইলে — পর্ব ১১-এ সেটা দেখবেন।

মূল ধারণা

ফাংশনের declaration একটি চুক্তি, আর চুক্তি যত স্পষ্ট, ভুল তত কম। টাইপ লিখে দেওয়া মানে compiler-কে পাহারাদার বানানো: bill("পাঁচশো", 2) লিখলে চালানোর আগেই error. একটি ভালো ফাংশন একটি কাজ করে, তার নাম সেই কাজ বলে দেয়, আর তার parameter আর return টাইপ দেখেই ব্যবহারকারী বুঝে যান কীভাবে ডাকতে হবে।

২ · Pass-by-value — ফাংশন পায় একটি ফটোকপি

ফাংশন ডাকলে argument-এর মান সরাসরি ফাংশনে যায় না; যায় তার একটি কপি. Parameter হলো নতুন একটি ভ্যারিয়েবল, যার প্রথম মান argument থেকে কপি করা। একে বলে pass by value, আর C++-এ এটাই ডিফল্ট।

void addGrace(int marks) {
    marks = marks + 5;          // শুধু কপিটা বদলাল
}

int rahim = 38;
addGrace(rahim);
// rahim এখনো 38 — আসল খাতায় কেউ হাত দেয়নি

পরীক্ষার খাতার ফটোকপি করে একজন শিক্ষককে দিলেন। তিনি ফটোকপিতে লাল কালিতে ৫ নম্বর বাড়িয়ে দিলেন। আসল খাতায় কি নম্বর বাড়ল? না। Pass-by-value ঠিক এমন — ফাংশন যা খুশি করুক, আসল ভ্যারিয়েবল নিরাপদ।

এটা একই সাথে সুবিধা আর সীমাবদ্ধতা। সুবিধা: ফাংশন ভুল করেও বাইরের ডেটা নষ্ট করতে পারে না, তাই কোড বোঝা সহজ। সীমাবদ্ধতা দুটি — (১) আসল ভ্যারিয়েবল বদলানো যায় না, (২) বড় জিনিস (যেমন লাখ অক্ষরের একটি std::string) প্রতিবার কপি হলে সময় আর মেমরি খরচ হয়। int বা double-এর মতো ছোট টাইপে কপির খরচ নগণ্য, তাই এগুলো by value পাঠানোই স্বাভাবিক।

পরের পর্বের ঝলক: দুটি সীমাবদ্ধতার সমাধান একই — reference. void addGrace(int& marks) লিখলে ফাংশন কপি নয়, আসল খাতাটাই পায়। আর const std::string& দিলে কপি ছাড়াই পড়া যায়, বদলানো যায় না। পর্ব ১০-এ বিস্তারিত দেখবেন; এই পর্বের কোডে সব কিছু by value.

৩ · Call stack — প্রতিটি ডাকের নিজের খাতা

একটি ফাংশন আরেকটি ফাংশনকে ডাকতে পারে, সেটি আবার আরেকটিকে। কম্পিউটার কীভাবে মনে রাখে কে কাকে ডেকেছে, আর কাজ শেষে কোথায় ফিরতে হবে? উত্তর: call stack. প্রতিটি ডাকের জন্য মেমরিতে একটি নতুন খাতা খোলে — একে বলে stack frame. এতে থাকে সেই ডাকের parameter, তার local variable আর কাজ শেষে কোথায় ফিরতে হবে সেই ঠিকানা। নতুন ডাক মানে আগের খাতার ওপর নতুন খাতা; return মানে ওপরের খাতাটি সরিয়ে ফেলা।

অফিসের টেবিলে ফাইলের স্তূপ। বড় সাহেব একটি ফাইল (main) নিয়ে কাজ করছেন। মাঝপথে একটি হিসাব লাগল, তিনি সেই ফাইলের ওপর আরেকটি ফাইল (bill) রাখলেন। সেটার কাজে আবার VAT-এর হিসাব লাগল — আরেকটি ফাইল (vat) ওপরে। সবার ওপরেরটা শেষ হলে সেটা সরিয়ে ঠিক নিচেরটায় ফিরে যান। সবশেষে যেটা রাখা হয়েছে, সেটাই সবার আগে শেষ হয়।

নিচে main ডাকছে bill-কে, আর bill ডাকছে vat-কে। প্রতিটি ধাপে stack-এ কী আছে, দেখুন:

1double vat(double amount) {2    return amount * 0.15;3}4double bill(double price, int qty) {5    double sub = price * qty;6    return sub + vat(sub);7}8int main() {9    double t = bill(500, 2);10}
প্রতিটি frame-এর ভ্যারিয়েবল
main: t
bill: price
bill: qty
bill: sub
vat: amount
  1. শুরুতে শুধু main. প্রোগ্রাম চালু হলে main-এর frame তৈরি হয়, তাতে t-এর জায়গা আছে, মান এখনো আসেনি। লাইন ৯-এ bill(500, 2) ডাকা হচ্ছে।
  2. bill-এর নতুন frame. stack-এর ওপরে নতুন খাতা খুলল। Argument 500 আর 2 কপি হয়ে বসল parameter price আর qty-তে। main এখন অপেক্ষা করছে — তার কাজ থেমে আছে লাইন ৯-এর মাঝখানে।
  3. Local ভ্যারিয়েবল জন্মাল। sub শুধু bill-এর frame-এর ভেতরে থাকে, মান 1000. main থেকে sub নামটি দেখাই যায় না।
  4. আরও এক ধাপ গভীরে — vat. vat(sub) ডাকায় তৃতীয় frame. sub-এর মান কপি হয়ে গেল amount-এ। এখন stack-এ তিনটি খাতা; ওপরেরটিই একমাত্র সক্রিয়, নিচের দুটি নিজের নিজের জায়গায় থেমে আছে।
  5. vat শেষ, frame মুছে গেল। 150 ফেরত গেল ঠিক যেখান থেকে ডাকা হয়েছিল — লাইন ৬-এ। amount নামের ভ্যারিয়েবলটির আর কোনো অস্তিত্ব নেই। bill আবার চলতে শুরু করল: 1000 + 150.
  6. bill-ও শেষ। 1150 ফেরত গেল main-এ, t-এ বসল। price, qty, sub — সব মুছে গেছে। stack আবার শুধু main. শেষে যে এসেছিল (vat), সে-ই আগে গেছে — একে বলে LIFO, Last In First Out.

Scope আর lifetime — কে কোথায় দেখা যায়, কতক্ষণ বাঁচে

উপরের ধাপগুলো থেকে দুটি আলাদা ধারণা বেরিয়ে আসে। Scope হলো কোডের কোন অংশ থেকে একটি নাম দেখা যায় — sub শুধু bill-এর { }-এর ভেতরে। Lifetime হলো কতক্ষণ জিনিসটি মেমরিতে বেঁচে থাকে — local ভ্যারিয়েবল জন্মায় ডাকের সাথে, মরে return-এর সাথে। এ কারণেই দুটি ভিন্ন ফাংশনে একই নাম sub রাখলে কোনো সংঘর্ষ হয় না: দুটি আলাদা খাতা, আলাদা ঘর।

যেহেতু return-এর সাথে frame মুছে যায়, তাই কোনো ফাংশন তার local ভ্যারিয়েবলের ঠিকানা বা reference ফেরত দিলে সেটি এমন জায়গা দেখায়, যেখানে আর কিছু নেই — একে বলে dangling reference, আর তা ব্যবহার করা undefined behavior. মান (কপি) ফেরত দেওয়া সবসময় নিরাপদ। পর্ব ১০-এ reference শেখার সময় আর পর্ব ১৮-এ lifetime-এর পূর্ণ আলোচনায় এই বিপদ আবার দেখবেন।

Global, static local আর ব্লকের ভেতরের ভ্যারিয়েবলের scope ও lifetime-এর পূর্ণ নিয়ম C কোর্সের স্কোপ ও স্টোরেজ ক্লাস পর্বে বিস্তারিত আছে; C++-এ নিয়মগুলো একই। শুধু মনে রাখুন: ফাংশনের যা দরকার, তা parameter দিয়ে নিন; যা দেওয়ার, তা return দিয়ে দিন — global ভ্যারিয়েবলের মাধ্যমে লুকিয়ে ডেটা আদান-প্রদান নয়।

৪ · Default argument — না বললে যা ধরে নেওয়া হবে

বেশিরভাগ বিক্রিতে পণ্য একটি, আর VAT ১৫%. প্রতিবার bill(200, 1, 0.15) লেখা বিরক্তিকর। C++ আপনাকে parameter-এর জন্য আগে থেকে একটি মান ঠিক করে দিতে দেয় — ডাকার সময় argument না দিলে সেটাই বসবে। একে বলে default argument.

রিকশাওয়ালাকে শুধু বললেন “নিউমার্কেট।” তিনি ধরে নিলেন আপনি একা যাবেন, সোজা পথে। আলাদা কিছু চাইলে আপনাকে বলতে হবে — “দুজন যাব”, “ঘুরে যান।” Default argument এমন “না বললে যা ধরে নেওয়া হয়” মান।

কিন্তু নিয়ম আছে, আর নিয়মগুলো আসে একটি সহজ সীমাবদ্ধতা থেকে: compiler argument-গুলো বাম থেকে ডানে মেলায়। ধাপে ধাপে দেখুন compiler কীভাবে খালি ঘর ভরায়:

1double bill(double price, int qty = 1, double vatRate = 0.15);2bill(200);3bill(200, 3);4bill(200, 3, 0.0);5bill(200, , 0.05);6double bad(double price = 100, int qty);
parameter-এ যা বসল
price
qty
vatRate
ফল
  1. শুধু দাম দিলেন। bill(200)-এ একটিমাত্র argument, সেটা বসল প্রথম parameter price-এ। বাকি দুটি ঘর খালি, তাই compiler declaration থেকে default মান এনে বসাল — ডাকটা আসলে হয়ে গেল bill(200, 1, 0.15). ফল ২৩০.
  2. দাম আর সংখ্যা দিলেন। 3 বসল qty-তে, default 1-এর জায়গায়। শুধু vatRate default থেকে এল। ফল ৬৯০.
  3. সব দিলেন। VAT-মুক্ত পণ্যের জন্য 0.0 দেওয়া হলো। Argument দিলে default-এর কোনো ভূমিকা নেই। ফল ৬০০.
  4. মাঝখানের ঘর বাদ দেওয়া যায় না। “সংখ্যা default থাকুক, শুধু VAT বদলাই” — এভাবে bill(200, , 0.05) লেখা C++-এ সম্ভবই নয়, এটা ব্যাকরণগত error. Argument বাম থেকে ডানে বসে; vatRate বদলাতে হলে তার আগের qty-ও লিখতে হবে: bill(200, 1, 0.05).
  5. Default শুধু ডান দিক থেকে। একই কারণে প্রথম parameter-এ default দিয়ে পরেরটায় না দিলে bad(…) ডাকার কোনো উপায় থাকে না — bad(5)-এর 5 তো price-এই বসবে, qty খালি থাকবে। তাই compiler declaration-ই প্রত্যাখ্যান করে। নিয়ম: কোনো parameter-এ default দিলে তার ডানের সবগুলোতেও দিতে হবে।

Default কোথায় লিখবেন — একবার, declaration-এ

Default মান ব্যবহার হয় ডাকার জায়গায় — compiler ডাকটা compile করার সময় ফাঁকা ঘর ভরে দেয়। তাই ডাকার জায়গা থেকে যা দেখা যায়, অর্থাৎ declaration-এ, default লিখতে হয়। একই default declaration আর definition দুই জায়গায় লিখলে compiler “default argument given for parameter … redefinition” ধরনের error দেয়। প্রচলিত রীতি: header-এর declaration-এ default, .cpp-এর definition-এ নয় (সেখানে চাইলে comment-এ লিখে রাখুন: int qty /* = 1 */)।

Default argument-এর অপব্যবহার: report(data, true, false, true) — এই তিনটি bool কী বোঝায়? কেউ জানে না। একগাদা default-ওয়ালা parameter, বিশেষ করে bool “flag”, কোডকে ধাঁধা বানায়। দুই-তিনটির বেশি default লাগলে ভাবুন: আলাদা নামের দুটি ফাংশন, বা একটি struct-এ সেটিংস (পর্ব ১২) কি পরিষ্কার হবে না?

৫ · Function overloading — এক নাম, ভিন্ন parameter-তালিকা

বৃত্তের ক্ষেত্রফল আর আয়তক্ষেত্রের ক্ষেত্রফল — দুটোই “ক্ষেত্রফল।” C-তে দুটির আলাদা নাম রাখতে হতো: area_circle, area_rect. C++-এ দুটিরই নাম রাখা যায় area, যদি তাদের parameter-তালিকা আলাদা হয়। একে বলে function overloading.

বাংলায় “চালানো” শব্দটি ভাবুন — গাড়ি চালানো, দোকান চালানো, কম্পিউটার চালানো। একই শব্দ, কিন্তু কী চালাচ্ছেন (বস্তুটা) দেখে আপনি অর্থ বুঝে যান। Compiler-ও তাই করে: নাম এক, কিন্তু argument কী ধরনের তা দেখে বুঝে নেয় কোন area ডাকা হচ্ছে।

Overload আলাদা হতে পারে parameter-এর সংখ্যায় (area(r) বনাম area(w, h)) বা টাইপে (maxOf(int, int) বনাম maxOf(double, double)). নাম আর parameter-টাইপের এই তালিকাকে বলে function signature. নিচের কোডটি চালিয়ে দেখুন:

#include <print>
#include <string>

double area(double r) { return 3.14159 * r * r; }     // circle
double area(double w, double h) { return w * h; }     // rectangle

int maxOf(int a, int b) { return a > b ? a : b; }
double maxOf(double a, double b) { return a > b ? a : b; }
std::string maxOf(std::string a, std::string b) { return a > b ? a : b; }

int main() {
    std::println("circle r=2      : {:.2f}", area(2.0));
    std::println("rectangle 3x4   : {}", area(3.0, 4.0));
    std::println("maxOf(7, 3)     : {}", maxOf(7, 3));
    std::println("maxOf(2.5, 9.1) : {}", maxOf(2.5, 9.1));
    std::println("maxOf(names)    : {}", maxOf("Rahim", "Karim"));
}

প্রথম দুটি লাইনে একই নাম area, কিন্তু argument একটি হলে বৃত্তের সূত্র (12.57), দুটি হলে আয়তক্ষেত্রের (12). maxOf(7, 3)-এ দুটিই int, তাই int-সংস্করণ চলে, ফল 7. maxOf(2.5, 9.1) double-সংস্করণে যায়, ফল 9.1. শেষ লাইনে "Rahim" আর "Karim" আসলে অক্ষরের array; int বা double-এ এরা বদলাতে পারে না, কিন্তু std::string-এ পারে — তাই string-সংস্করণ বেছে নেওয়া হয়, আর অভিধানের ক্রমে বড় Rahim (R আসে K-এর পরে)। প্রতিটি ডাকের সিদ্ধান্ত হয়েছে compile-এর সময়েই, প্রোগ্রাম চলার সময় কোনো খোঁজাখুঁজি নেই।

শুধু return টাইপ আলাদা হলে overload হয় না

int    parse(std::string s);
double parse(std::string s);   // error: ambiguating new declaration

কেন? কারণ ডাকার সময় return টাইপ প্রায়ই দেখা যায় না। parse("42"); — ফল কোথাও রাখা হয়নি, তাহলে কোনটা ডাকবে? auto x = parse("42"); — auto তো নিজেই ফল দেখে টাইপ ঠিক করতে চায়! Compiler argument দেখে বাছাই করে, ফল কোথায় যাবে তা দেখে নয়। তাই overload আলাদা হতে হবে parameter-তালিকায়। আলাদা ফল চাইলে আলাদা নাম দিন: parseInt, parseDouble.

ভেতরে কী হয়: linker-এর কাছে প্রতিটি ফাংশনের একটি অনন্য নাম লাগে, অথচ এখানে দুটি ফাংশনই area. তাই compiler নামের সাথে parameter-টাইপ জুড়ে নতুন নাম বানায় — একে বলে name mangling. GCC আর Clang-এ area(double) হয়ে যায় _Z4aread, আর area(double, double) হয় _Z4areadd (d মানে double). Return টাইপ এই নামে থাকে না — আরেকটি কারণ, কেন শুধু return টাইপে overload চলে না। ভাবনার প্রশ্ন ১-এ এর ইতিহাস দেখুন।

C জানলে

ফাংশন, prototype, pass-by-value, call stack আর recursion — এগুলো C আর C++-এ হুবহু এক। নতুন হলো overloading আর default argument; C-তে দুটির কোনোটিই নেই (C11-এর _Generic দিয়ে টাইপ দেখে বাছাইয়ের একটি সীমিত উপায় আছে)। আরেকটি সূক্ষ্ম পার্থক্য: পুরোনো C-তে int f(); মানে “parameter অজানা”, কিন্তু C++-এ মানে “কোনো parameter নেই” (C23 থেকে C-ও এই অর্থ নিয়েছে)। Prototype আর ফাংশন ভাগ করার গভীর আলোচনা C কোর্সের ফাংশন — কাজ ভাগ করে দেওয়া পর্বে।

৬ · Compiler কোনটা বেছে নেয় — overload resolution

Argument-এর টাইপ সবসময় কোনো overload-এর সাথে হুবহু মেলে না। print('A') ডাকলে, অথচ শুধু print(int) আর print(double) থাকলে, কোনটা চলবে? Compiler-এর এই বাছাই প্রক্রিয়ার নাম overload resolution. পূর্ণ নিয়ম standard-এর অনেক পৃষ্ঠা জুড়ে, কিন্তু মূল ধারণা একটি সিঁড়ি:

ধাপমিলের ধরনউদাহরণ
১ (সবচেয়ে ভালো)Exact match — টাইপ হুবহু এক42 → int, 3.14 → double
২Promotion — ছোট টাইপকে তার স্বাভাবিক বড় টাইপে তোলা, কোনো তথ্য হারায় নাchar/short/bool → int; float → double
৩Standard conversion — অন্য সংখ্যা-টাইপে বদল (implicit conversion)int → double, double → int, long → int
৪User-defined conversion — কোনো class-এর নিজস্ব রূপান্তর"Dhaka" → std::string
—কোনো পথ নেই"Dhaka" → int: এই overload বাদ

Compiler প্রতিটি overload-এর জন্য দেখে, argument থেকে parameter-এ যেতে কোন ধাপ লাগে। যার ধাপ সবচেয়ে ওপরে, সে জেতে। দুজন সমান ধাপে থাকলে কেউ জেতে না — compile error. তিনটি overload নিয়ে ছয়টি ডাক ধাপে ধাপে দেখুন, প্রতিবার আগে নিজে অনুমান করুন:

1void print(int x);2void print(double x);3void print(std::string s);4print(42);5print(3.14);6print('A');7print(2.5f);8print("Dhaka");9print(10L);10print(static_cast<int>(10L));
প্রতিটি overload-এর মিল
print(int)
print(double)
print(string)
  1. 42 — সহজ জয়। 42 একটি int literal, print(int)-এর সাথে exact match. print(double)-ও চলত (conversion দিয়ে), কিন্তু exact match সবসময় ওপরে।
  2. 3.14 — উল্টোটা। দশমিক literal হলো double, তাই এবার print(double) exact. print(int)-এ যেতে হলে দশমিক কাটতে হতো — conversion, নিচের ধাপ।
  3. 'A' — অবাক করা ফল। print(char) নেই। char থেকে int হলো promotion (ধাপ ২), কিন্তু char থেকে double শুধু conversion (ধাপ ৩)। তাই print(int) জেতে, আর স্ক্রিনে আসে A-এর কোড 65, অক্ষর নয়!
  4. 2.5f — float-এর পথ। float থেকে double promotion, কিন্তু float থেকে int conversion. তাই print(double) জেতে। একই মান, শুধু শেষের f বদলে দিল কোন পথ “ভালো।”
  5. "Dhaka" — একমাত্র পথ। অক্ষরের array থেকে int বা double-এ যাওয়ার কোনো নিয়ম নেই, দুটি overload সরাসরি বাদ। std::string-এ যাওয়া যায় class-এর নিজস্ব রূপান্তর দিয়ে — সবচেয়ে নিচের ধাপ, কিন্তু একমাত্র প্রার্থী, তাই সে-ই জেতে।
  6. 10L — টাই, তাই error. 10L হলো long. long থেকে int-ও conversion, long থেকে double-ও conversion — দুটি সমান ধাপে। “বেশি কাছের” বলে কিছু নেই; compiler অনুমান করে না, সোজা বলে দেয় ambiguous call. এটা আসলে ভালো খবর — নীরবে ভুলটা বেছে নেওয়ার চেয়ে থেমে যাওয়া নিরাপদ।
  7. সমাধান — নিজে স্পষ্ট করুন। static_cast<int>(10L) দিয়ে argument-কে int বানালে আবার exact match. আরেক উপায় হলো print(long) নামে চতুর্থ overload যোগ করা। কোনটা ঠিক, তা নির্ভর করে আপনি আসলে কী চান — এই সিদ্ধান্ত compiler নয়, আপনি নেবেন।

Overloading আর default argument একসাথে — টাইয়ের আরেক উৎস

void show(int x);
void show(int x, int width = 0);

show(5);       // error: call of overloaded 'show(int)' is ambiguous

দুটি declaration আলাদাভাবে বৈধ, কিন্তু show(5) দুটির সাথেই exact match — দ্বিতীয়টির ফাঁকা ঘর তো default দিয়ে ভরে যায়। মজার ব্যাপার, error আসে ডাকার জায়গায়, declaration-এ নয়; তাই অনেক দিন কেউ show(5) না ডাকলে সমস্যাটা লুকিয়েই থাকে। নিয়ম: একই কাজের জন্য হয় overload, নয় default argument — দুটো মিশিয়ে নয়।

Overloading C++-এর একেবারে প্রথম দিকের বৈশিষ্ট্য। Bjarne Stroustrup-এর বিবরণ অনুযায়ী শুরুর দিকের C++-এ overloaded নামের আগে আলাদা করে overload নামে একটি শব্দ লিখে জানাতে হতো, পরে অভিজ্ঞতা থেকে সেই শর্ত তুলে দেওয়া হয়। int আর double-এর মতো সংখ্যা-টাইপের মধ্যে কোনটা “ভালো মিল”, সেই নিয়ম নিয়ে বছরের পর বছর তর্ক চলেছে — কারণ C থেকে পাওয়া অসংখ্য নিঃশব্দ রূপান্তর-নিয়ম প্রতিটি সিদ্ধান্তকে জটিল করে। আজকের ধাপভিত্তিক নিয়ম সেই আপসের ফল।

৭ · Recursion — নিজেকেই ডাকা

একটি ফাংশন নিজেকেও ডাকতে পারে — একে বলে recursion. শর্ত দুটি: (১) প্রতিটি ডাকে সমস্যা একটু ছোট হবে, (২) একটি সবচেয়ে ছোট অবস্থা থাকবে, যেখানে আর না ডেকে সরাসরি উত্তর দেওয়া যায় — একে বলে base case. ক্লাসিক উদাহরণ factorial: ৪! = ৪ × ৩!, আর ১! = ১.

long long factorial(int n) {
    if (n <= 1) return 1;              // base case
    return n * factorial(n - 1);       // ছোট সমস্যা নিজেকে দিয়ে
}

অংশ ৩-এর call stack-ই এখানে আসল চাবি। প্রতিটি ডাক নিজের frame পায়, নিজের n পায় — তাই চারটি ভিন্ন n একসাথে বেঁচে থাকতে পারে:

1long long factorial(int n) {2    if (n <= 1) return 1;3    return n * factorial(n - 1);4}
অবস্থা
stack-এ frame
সর্বশেষ ফেরত মান
  1. factorial(4) শুরু। n = 4, base case নয়। হিসাবটা 4 * factorial(3) — কিন্তু factorial(3)-এর উত্তর এখনো জানা নেই, তাই গুণটা অপেক্ষায় থেমে রইল।
  2. এক ধাপ নিচে। নতুন frame, নিজের n = 3. আগের frame-এর n = 4 হারায়নি, নিচে নিরাপদে আছে। আবার অপেক্ষা: 3 * ?
  3. আরও নিচে। n = 2, তিনটি frame জমেছে, তিনটি অসম্পূর্ণ গুণ অপেক্ষায়।
  4. Base case — থামার জায়গা। n = 1, শর্ত সত্য, তাই আর ডাক নয়, সরাসরি 1 ফেরত। এই লাইনটি না থাকলে ডাক চলতেই থাকত — n শূন্য, ঋণাত্মক… যতক্ষণ না stack-এর জায়গা ফুরিয়ে প্রোগ্রাম crash করে (stack overflow).
  5. খোলা শুরু। 1 ফিরে গেল factorial(2)-এর অপেক্ষমাণ গুণে: 2 * 1 = 2. সেই frame-ও শেষ, মুছে গেল।
  6. আরও ওপরে। 3 * 2 = 6, ফেরত গেল নিচের দিকে নয়, ডাকার দিকে — factorial(4)-এর কাছে।
  7. উত্তর। 4 * 6 = 24. যাওয়ার পথে শুধু প্রশ্ন জমেছে, ফেরার পথে উত্তর তৈরি হয়েছে। Recursion বোঝার মূল কথা এটাই — stack ভরা আর stack খোলা।

Fibonacci-র recursion (fib(n) = fib(n-1) + fib(n-2)) দেখতে আরও সুন্দর, কিন্তু একই মান বারবার হিসাব করে — fib(40)-এ ফাংশন-ডাক দাঁড়ায় কোটির ঘরে। লুপ বা আগের ফল মনে রাখার কৌশল (memoization) দিয়ে সেটা চোখের পলকে হয়। Recursion কখন ভালো আর কখন লুপ ভালো, tail call আর গভীর recursion-এর বিপদ — এসব C কোর্সের রিকার্শন — নিজেকেই ডাকা পর্বে বিস্তারিত।

৮ · আধুনিক ফাংশন-লেখা — auto, nodiscard, inline, constexpr

auto return আর trailing return

পর্ব ০৫-এ ভ্যারিয়েবলের জন্য auto দেখেছেন। C++14 থেকে ফাংশনের return টাইপেও auto লেখা যায় — compiler return-এর মান দেখে টাইপ ঠিক করে। একে বলে return type deduction. আর C++11 থেকে return টাইপ নামের পরে, -> দিয়েও লেখা যায় — trailing return type:

auto square(int x) { return x * x; }          // compiler ঠিক করে: int
auto area(double r) -> double {               // trailing: টাইপ নামের পরে
    return 3.14159 * r * r;
}
auto pick(bool b) { if (b) return 1; return 2.5; }  // error: int আর double দুই রকম

শেষ লাইনটি খেয়াল করুন: সব return-কে একই টাইপ দিতে হবে, নইলে compiler সিদ্ধান্ত নিতে পারে না। ছোট, স্পষ্ট ফাংশনে auto return চলে; কিন্তু অন্যের ব্যবহারের জন্য লেখা ফাংশনে (বিশেষ করে header-এ) টাইপটা লিখে দিন, কারণ declaration-ই চুক্তি — চুক্তিতে “যা হয় হবে” লেখা ভালো নয়। Trailing return-এর আসল কাজ template-এ, যেখানে return টাইপ parameter-এর টাইপের ওপর নির্ভর করে (যেমন -> decltype(a + b); template শিখবেন পর্ব ২৮-এ); আর lambda-য় (পর্ব ৩৪) এটাই একমাত্র লেখার উপায়।

[[nodiscard]] — “আমার ফল ফেলে দেবেন না”

কিছু ফাংশনের ফল উপেক্ষা করা প্রায় নিশ্চিত বাগ। bill(200); লিখে ফল কোথাও রাখলেন না — হিসাবটা হলো, তারপর হাওয়ায় মিলিয়ে গেল। C++17-এর attribute [[nodiscard]] দিয়ে চিহ্নিত করলে compiler সতর্ক করে দেয়:

warning: ignoring return value of 'double bill(double, int, double)', declared with attribute 'nodiscard' [-Wunused-result]

C++20 থেকে কারণও লিখে দেওয়া যায়: [[nodiscard("হিসাবের ফল ব্যবহার করুন")]]. Standard library নিজেও এটা ব্যবহার করে — C++20 থেকে v.empty() (“খালি কি না?” প্রশ্ন) [[nodiscard]], কারণ অনেকে ভুল করে একে “খালি করো” ভেবে ফল ছাড়াই লিখতেন।

inline — আজকের আসল অর্থ

নাম দেখে মনে হয় inline মানে “ফাংশন ডাকার বদলে কোডটা সেখানে বসিয়ে দাও, দ্রুত হবে।” ১৯৮০-৯০-এর দশকে এটাই উদ্দেশ্য ছিল। কিন্তু আজকের compiler এই সিদ্ধান্ত নিজেই নেয় — -O2 দিলে ছোট ফাংশন inline না লিখলেও বসিয়ে দেয় (optimization), আর বড় ফাংশনে inline লিখলেও প্রায়ই বসায় না। তাহলে শব্দটি এখন কী কাজে লাগে?

উত্তর: ODR (One Definition Rule). সাধারণ নিয়মে একটি ফাংশনের definition পুরো প্রোগ্রামে একবারই থাকতে পারে। কিন্তু header ফাইলে definition লিখে সেই header দুটি .cpp ফাইলে #include করলে definition দুবার হয়ে যায় — linker বলে “multiple definition.” inline function এই নিয়মে ছাড় পায়: প্রতিটি ফাইলে থাকতে পারে, যদি সব কপি হুবহু এক হয়। অর্থাৎ আধুনিক C++-এ inline মানে “এই definition header-এ থাকতে পারে”, গতির প্রতিশ্রুতি নয়। Class-এর ভেতরে লেখা member ফাংশন আর constexpr ফাংশন নিজে থেকেই inline. পর্ব ১১-এ header লেখার সময় এটা কাজে লাগবে।

constexpr ফাংশন — এক ঝলক

পর্ব ০৫-এ constexpr ভ্যারিয়েবল দেখেছেন। ফাংশনও constexpr হতে পারে: constexpr int square(int x) { return x * x; }. তখন argument compile-এর সময় জানা থাকলে compiler নিজেই ফল হিসাব করে প্রোগ্রামে বসিয়ে দেয় — চলার সময় কোনো খরচ নেই। আর argument চলার সময় এলে সাধারণ ফাংশনের মতোই চলে। পর্ব ৩৯-এ এর পূর্ণ শক্তি দেখবেন।

এবার সব একসাথে — declaration-এ default আর [[nodiscard]], main-এর পরে definition, আর একটি recursive ফাংশন:

#include <print>

// declaration: default argument shudhu ekhane
[[nodiscard]] double bill(double price, int qty = 1, double vatRate = 0.15);
[[nodiscard]] long long factorial(int n);

int main() {
    std::println("1 item      : {:.2f}", bill(200.0));
    std::println("3 items     : {:.2f}", bill(200.0, 3));
    std::println("3 items, 0% : {:.2f}", bill(200.0, 3, 0.0));
    std::println("10!         : {}", factorial(10));
}

double bill(double price, int qty, double vatRate) {   // default abar noy
    return price * qty * (1.0 + vatRate);
}

long long factorial(int n) {
    if (n <= 1) return 1;                              // base case
    return n * factorial(n - 1);
}

main ফাংশনগুলোকে ওপরের declaration দেখেই ডাকতে পারছে, যদিও definition আছে নিচে। প্রথম তিন লাইনে আসবে 230.00, 690.00 আর 600.00 — compiler ফাঁকা ঘরগুলো default দিয়ে ভরেছে। Definition-এ default আবার লেখা হয়নি; লিখলে compile error হতো। শেষ লাইনে 10! = 3628800 — দশটি frame জমে আবার খুলেছে। পরীক্ষা করে দেখুন: main-এ শুধু bill(50.0); লিখে ফল ফেলে দিন — [[nodiscard]]-এর সতর্কবার্তা আসবে।

পরীক্ষা ও ইন্টারভিউতে
  • Parameter আর argument-এর পার্থক্য? — Parameter ফাংশনের সংজ্ঞায় লেখা নাম; argument ডাকার সময় পাঠানো আসল মান।
  • শুধু return টাইপ আলাদা করে কি overload করা যায়? — না; overload আলাদা হতে হয় parameter-এর সংখ্যা বা টাইপে।
  • void f(int a = 1, int b); বৈধ কি? — না; কোনো parameter-এ default দিলে তার ডানের সবগুলোতেও দিতে হয়।
  • Overloading আর overriding-এর পার্থক্য? — Overloading হলো একই scope-এ এক নাম, ভিন্ন parameter (compile-এর সময় বাছাই); overriding হলো derived class-এ base-এর virtual ফাংশন নতুন করে লেখা (চলার সময় বাছাই, পর্ব ২৫)।
  • Recursion-এ base case না থাকলে কী হয়? — ডাক থামে না, call stack ফুরিয়ে stack overflow-তে প্রোগ্রাম crash করে।

৯ · AI-এর যুগে — ফাংশনের চুক্তি যাচাই করা আপনার কাজ

AI টুল ফাংশন লেখে চমৎকার গতিতে, আর বেশিরভাগ সময় সেগুলো compile-ও হয়। সমস্যা হলো ফাংশনের ভুল প্রায়ই চুক্তিতে — কোন overload কখন বাছাই হবে, default কী ধরে নিচ্ছে, ফেরত দেওয়া জিনিস আদৌ বেঁচে আছে কি না। এগুলো চোখে না পড়লে ধরা পড়ে অনেক পরে, অন্য কারও কোডে। AI-এর লেখা C++ ফাংশনে বারবার দেখা যায়:

AI যা করেসমস্যাযা হওয়া উচিত
f(int), f(long), f(double) — একগাদা সংখ্যা-overloadf(5u) বা f('A')-এর মতো ডাকে ambiguous error, বা অপ্রত্যাশিত overload বাছাইএকটি টাইপে একটি ফাংশন, বা স্পষ্ট নাম; দরকারে পরে template (পর্ব ২৮)
f(int x) আর f(int x, int y = 0) একসাথেf(5) ambiguousoverload বা default — যেকোনো একটি
পাঁচ-ছয়টি default parameter, তার মধ্যে কয়েকটি boolsave(d, true, false, true) — পড়ে অর্থ বোঝা যায় নাআলাদা নামের ফাংশন, enum class বা settings struct (পর্ব ১২)
Local ভ্যারিয়েবলের reference বা pointer ফেরতreturn-এর পরে frame মুছে যায় — dangling, undefined behaviorমান (কপি) ফেরত দিন; পর্ব ১০ ও ১৮ দেখুন
গুরুত্বপূর্ণ ফল দেওয়া ফাংশনে [[nodiscard]] নেই; ডাকার জায়গায় ফল উপেক্ষাerror-কোড বা হিসাব নীরবে হারায়[[nodiscard]] দিন, warning-কে error হিসেবে ধরুন
Global ভ্যারিয়েবল পড়ে-লিখে কাজ করা লম্বা ফাংশনলুকানো নির্ভরতা, পরীক্ষা করা কঠিনইনপুট parameter-এ, ফল return-এ; এক ফাংশন এক কাজ

AI-কে ফাংশন লিখতে বলার সময় চুক্তির নিয়মগুলো আগেই বলে দিন, আর ফলাফলে প্রতিটি ডাকের ব্যাখ্যা চান:

আমি C++23 শিখছি (g++, -Wall -Wextra). একটি দোকানের জন্য ফাংশন লেখো: ক্ষেত্রফল (বৃত্ত ও আয়তক্ষেত্র) আর VAT-সহ বিল। নিয়ম: ১. প্রতিটি ফাংশন একটি কাজ করবে; ইনপুট parameter-এ, ফল return-এ — কোনো global ভ্যারিয়েবল নয়। ২. overload শুধু তখনই, যখন parameter-এর সংখ্যা আলাদা; int/long/double-এর মতো কাছাকাছি টাইপে overload করবে না। ৩. একই ফাংশনে overload আর default argument মেশাবে না; default থাকলে শুধু declaration-এ, সর্বোচ্চ দুটি। ৪. কোনো local ভ্যারিয়েবলের reference বা pointer ফেরত দেবে না। ৫. যে ফাংশনের ফল ফেলে দেওয়া ভুল, তাতে [[nodiscard]] দেবে। কোডের পরে একটি তালিকা দাও: main-এর প্রতিটি ডাকে কোন overload চলবে আর কেন (exact match, promotion না conversion).

AI-এর লেখা ফাংশন যাচাইয়ের চেকলিস্ট

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

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

প্র ০১Linker তো শুধু নাম দেখে জোড়া লাগায় — তাহলে একই নামের তিনটি maxOf সে আলাদা করে কীভাবে? Name mangling ছাড়া overloading কি সম্ভব হতো?

পর্ব ০৩-এ দেখেছেন, compiler প্রতিটি .cpp ফাইলকে আলাদা object ফাইলে বদলায়, আর linker সেগুলো জোড়ে নাম মিলিয়ে। Linker-এর জগৎ খুব সরল: প্রতিটি symbol-এর একটি নাম আর একটি ঠিকানা। সে C++-এর টাইপ বোঝে না, overloading বোঝে না। তাহলে maxOf(int, int) আর maxOf(double, double) — দুটিই যদি maxOf নামে যেত, linker-এর কাছে সেটা “multiple definition” হয়ে যেত।

সমাধান হলো compiler নিজেই নামের মধ্যে টাইপের তথ্য ঢুকিয়ে দেয়। Linux আর macOS-এ GCC ও Clang যে নিয়ম মানে (Itanium C++ ABI), তাতে maxOf(int, int) হয় _Z5maxOfii: _Z মানে “এটি mangled নাম”, 5 হলো নামের দৈর্ঘ্য, তারপর নাম, তারপর প্রতিটি parameter-এর একটি সংকেত (i = int, d = double). Double-সংস্করণ হয় _Z5maxOfdd. এখন linker-এর কাছে এরা সম্পূর্ণ আলাদা দুটি নাম। Windows-এ MSVC সম্পূর্ণ ভিন্ন একটি নিয়ম মানে — তাই GCC আর MSVC-তে বানানো C++ লাইব্রেরি সরাসরি মেশানো যায় না।

নিজে দেখতে চাইলে g++ -c overload.cpp দিয়ে object ফাইল বানিয়ে nm overload.o চালান — mangled নামগুলো দেখবেন। nm -C বা c++filt দিয়ে আবার পড়ার মতো নামে ফেরানো যায়। Linker error-এ যে লম্বা অদ্ভুত নাম আসে, সেগুলো এই mangled নামই।

ঐতিহাসিকভাবে এর প্রয়োজন আরও স্পষ্ট ছিল। Stroustrup-এর প্রথম C++ compiler, Cfront, C++ কোডকে C কোডে অনুবাদ করত, তারপর সাধারণ C compiler আর linker ব্যবহার হতো। C-তে তো একই নামের দুটি ফাংশন অসম্ভব, তাই Cfront-কে প্রতিটি overload-কে বৈধ, অনন্য C নাম দিতে হতো। Mangling সেখান থেকেই জন্মায় — বিদ্যমান টুলচেইন না বদলে নতুন ভাষা চালানোর কৌশল।

উল্টো দিকও আছে। C++ থেকে C লাইব্রেরি ডাকতে, বা C++ ফাংশন অন্য ভাষাকে দিতে, mangling বন্ধ করতে হয় — সেজন্য extern "C". তখন ফাংশনটির নাম হুবহু থাকে, কিন্তু বিনিময়ে সেটিকে আর overload করা যায় না।

মূল উপলব্ধি: Overloading আসলে compiler-এর একটি নাম-বদলের কৌশল; linker কিছুই নতুন শেখেনি। ভাষার অনেক “জাদু” এভাবে পুরোনো, সরল ব্যবস্থার ওপর বুদ্ধিমান অনুবাদ — আর সেই অনুবাদের নিয়ম (ABI) মেলানো বড় প্রজেক্টে আসল মাথাব্যথা।

প্র ০২C++ যখন overloading দিল, C কেন এত দশকেও তা নেয়নি? এটা কি C-এর দুর্বলতা, না সচেতন সিদ্ধান্ত?

C-এর মূল প্রতিশ্রুতি হলো সরলতা আর অনুমানযোগ্যতা: যা লেখা আছে, প্রায় হুবহু তা-ই মেশিনে যায়। C-তে একটি ফাংশনের নাম object ফাইলেও সেই নামই থাকে — printf মানে printf (কিছু প্ল্যাটফর্মে বড়জোর সামনে একটি underscore). এই সরলতার কারণেই C হয়ে উঠেছে প্রোগ্রামিং জগতের “সাধারণ ভাষা।” Python, Rust, Go, Java — প্রায় সব ভাষা অন্য ভাষার কোড ডাকতে C-এর নিয়ম (C ABI) ব্যবহার করে। অপারেটিং সিস্টেমের system call-এর ইন্টারফেসও C-ধাঁচের।

Overloading আনলে C-কেও name mangling নিতে হতো, আর তখন এই সর্বজনীন সামঞ্জস্য ভেঙে যেত — প্রতিটি compiler-কে একই mangling নিয়মে রাজি করানো লাগত, যা C++ নিজেও সব প্ল্যাটফর্মে পারেনি। সাথে লাগত overload resolution-এর জটিল নিয়ম। C-এর স্বয়ংক্রিয় টাইপ-রূপান্তর এমনিতেই অনেক; তার ওপর overloading বসালে “কোন ফাংশন চলবে” প্রশ্নের উত্তর C-এর ছোট্ট standard-এ আর ছোট থাকত না। C কমিটির ঘোষিত নীতির মধ্যেও আছে ভাষাকে ছোট ও সরল রাখা, আর বিদ্যমান কোড না ভাঙা।

তবে প্রয়োজনটা C-ও অনুভব করেছে। sqrt, sqrtf, sqrtl — একই কাজের তিনটি নাম, শুধু টাইপ আলাদা বলে। C99-এ <tgmath.h> আসে, যেখানে sqrt(x) লিখলে x-এর টাইপ দেখে সঠিকটি চলে — কিন্তু প্রথমে এটা ছিল compiler-এর গোপন কৌশল, সাধারণ প্রোগ্রামার নিজে এমন কিছু লিখতে পারতেন না। C11-এ _Generic এসে সেটা সবার হাতে দেয়: একটি macro argument-এর টাইপ দেখে আলাদা আলাদা নামের ফাংশন বেছে নিতে পারে। খেয়াল করুন, ফাংশনগুলোর নাম তখনও আলাদা থাকে — linker-এর জগৎ বদলায় না, শুধু ডাকার জায়গায় compile-এর সময় বাছাই হয়।

অর্থাৎ C “একই নামে ভিন্ন ফাংশন” দেয়নি, দিয়েছে “এক ডাক-নাম থেকে ভিন্ন নামের ফাংশনে পৌঁছানোর পথ।” পার্থক্যটা সূক্ষ্ম, কিন্তু C-এর দর্শনের সাথে পুরোপুরি মেলে।

মূল উপলব্ধি: একটি বৈশিষ্ট্য “না থাকা” সবসময় দুর্বলতা নয়। C-এর শক্তিই তার সরল, স্থির ABI — যে কারণে পৃথিবীর প্রায় সব সফটওয়্যার C-এর মাধ্যমে একে অপরের সাথে কথা বলে। C++ বেছে নিয়েছে প্রকাশের ক্ষমতা, C বেছে নিয়েছে সর্বজনীন সামঞ্জস্য।

প্র ০৩Default argument ডাকার জায়গায় compile-এর সময় বসে — এই সাধারণ তথ্য virtual ফাংশনের সাথে মিশলে কী অদ্ভুত ফল দিতে পারে?

এই পর্বে দেখেছেন, bill(200) লিখলে compiler ডাকার জায়গায় নিজে bill(200, 1, 0.15) বানিয়ে নেয়। অর্থাৎ default মান কোনো ফাংশনের “ভেতরে” থাকে না; থাকে ডাকার জায়গায়, আর বেছে নেওয়া হয় compile-এর সময়, compiler যে declaration দেখছে তা থেকে। সাধারণ ফাংশনে এতে কোনো সমস্যা নেই।

এবার পর্ব ২৫-এর virtual ফাংশনের কথা ভাবুন, যেখানে কোন ফাংশন চলবে তা ঠিক হয় প্রোগ্রাম চলার সময়, object আসলে কোন ধরনের তা দেখে। ধরুন base class Notifier-এ virtual void send(std::string channel = "SMS"), আর derived class EmailNotifier একে override করে লিখল void send(std::string channel = "Email"). এখন একটি Notifier-এর pointer বা reference, যা আসলে একটি EmailNotifier-কে দেখাচ্ছে, তা দিয়ে send() ডাকলেন।

কী হবে? ফাংশনের শরীর চলবে EmailNotifier-এর (virtual, চলার সময় বাছাই)। কিন্তু default মান আসবে Notifier-এর declaration থেকে — "SMS" — কারণ compiler ডাকার জায়গায় শুধু pointer-এর টাইপ Notifier দেখেছিল। ফল: Email-এর কোড চলছে "SMS" নিয়ে। দুই সিদ্ধান্ত দুই সময়ে, দুই জায়গা থেকে, আর তাদের মিশ্রণ এমন আচরণ তৈরি করে যা কোনো একক declaration পড়ে বোঝা যায় না।

এটা পুরোনো, সুপরিচিত ফাঁদ। Scott Meyers তাঁর Effective C++ বইয়ে একটি পুরো নিয়মই রেখেছেন — inherited default parameter-এর মান কখনো নতুন করে সংজ্ঞায়িত করবেন না। Google-এর C++ style guide আরও কড়া — virtual ফাংশনে default argument-ই নিষিদ্ধ। প্রচলিত সমাধান হলো non-virtual একটি public ফাংশনে default রেখে ভেতরে virtual ফাংশনকে সব argument দিয়ে ডাকা, যাতে default থাকে এক জায়গায়।

বড় শিক্ষাটা শুধু virtual নিয়ে নয়। যেকোনো ফিচার বোঝার সময় জিজ্ঞেস করুন: এটা কখন ঠিক হয় — compile-এর সময়, না চলার সময়? আর কোন তথ্য দেখে? Overload resolution, default argument, template — সব compile-এর সময়ের সিদ্ধান্ত, শুধু static টাইপ দেখে। Virtual dispatch চলার সময়ের সিদ্ধান্ত। দুই জগৎ যেখানে মেলে, সেখানেই বিস্ময়।

মূল উপলব্ধি: Default argument হলো ডাকার জায়গায় compiler-এর লিখে দেওয়া লেখা, ফাংশনের অংশ নয়। তাই যেখানে ফাংশন চলার সময় বদলায়, সেখানে default স্থির থেকে বিভ্রান্তি তৈরি করে — virtual ফাংশনে default argument এড়িয়ে চলুন।

প্র ০৪একটি ফাংশন ডাকতে আসলে কত খরচ? Optimizer যদি নিজেই ফাংশন বসিয়ে দেয় (inlining), তাহলে সব ফাংশন বসিয়ে দেয় না কেন?

একটি সাধারণ ফাংশন-ডাকে CPU-কে কয়েকটি কাজ করতে হয়: argument-গুলো নির্দিষ্ট জায়গায় রাখা (x86-64 Linux-এ প্রথম কয়েকটি argument যায় register-এ, বাকিগুলো stack-এ), call নির্দেশে ফেরার ঠিকানা stack-এ রেখে লাফ দেওয়া, ফাংশনের শুরুতে নতুন frame সাজানো, শেষে frame গুটিয়ে ret দিয়ে ফেরা। আধুনিক CPU-তে এটা সাধারণত মাত্র কয়েক ন্যানোসেকেন্ড বা তারও কম। একবার ডাকলে কিছুই না; কিন্তু একটি ছোট্ট ফাংশন যদি লুপে কোটিবার ডাকা হয়, খরচটা চোখে পড়তে পারে।

তবে আসল লাভ শুধু এই কয়েকটি নির্দেশ বাঁচানো নয়। ফাংশন ডাকের জায়গায় বসিয়ে দিলে (inlining) optimizer পুরো কোডটা একসাথে দেখতে পায়। square(5) বসিয়ে দিলে সে বোঝে এটা আসলে 25; লুপের ভেতরের ছোট ফাংশন বসালে লুপটাকে vectorize করতে পারে (একসাথে কয়েকটি সংখ্যা হিসাব)। অর্থাৎ inlining অন্য সব optimization-এর দরজা খুলে দেয়। এজন্যই -O2-তে GCC আর Clang ছোট ফাংশন নিয়মিত বসিয়ে দেয় — inline লেখা থাকুক বা না থাকুক।

তাহলে সব ফাংশন বসায় না কেন? কারণ প্রতিটি বসানো মানে ফাংশনের কোডের আরেকটি কপি। একটি বড় ফাংশন একশো জায়গায় বসালে প্রোগ্রাম ফুলে যায় (code bloat). CPU-র instruction cache ছোট; কোড বড় হলে cache-এ ধরে না, আর memory থেকে নির্দেশ আনার দেরি ডাকের খরচের চেয়ে অনেক বেশি হতে পারে। Recursive ফাংশন সম্পূর্ণ বসানো অসম্ভব, আর virtual ফাংশন চলার আগে জানা যায় না কোনটা চলবে। আবার অন্য .cpp ফাইলের ফাংশন compiler দেখতেই পায় না — সেজন্য আছে LTO (link-time optimization), যা link-এর সময় পুরো প্রোগ্রাম দেখে বসাতে পারে।

এ কারণেই compiler-কে বিচারক বানানো হয়েছে: সে ফাংশনের আকার, কতবার ডাকা হয়, লুপে আছে কি না — এসব মেপে সিদ্ধান্ত নেয়। মানুষের inline ইঙ্গিত গত কয়েক দশকে প্রায় অপ্রাসঙ্গিক হয়ে গেছে, তাই শব্দটির অর্থ সরে গেছে ODR-এর দিকে। খুব বিশেষ ক্ষেত্রে GCC-র __attribute__((always_inline)) বা noinline-এর মতো compiler-নির্দিষ্ট উপায় আছে।

মূল উপলব্ধি: “ফাংশন ডাকা ধীর, তাই বড় ফাংশন লিখি” — এই চিন্তা আজ প্রায় সবসময় ভুল। ছোট, পরিষ্কার ফাংশন লিখুন; গতির কাজ optimizer-এর। সন্দেহ হলে অনুমান নয়, মেপে দেখুন — পর্ব ৪৯-এ profiler দিয়ে সেটাই শিখবেন।

প্র ০৫“Pure function” কী, আর কেন অভিজ্ঞ প্রোগ্রামাররা বলেন যত বেশি সম্ভব ফাংশন pure রাখুন? Pure ফাংশন পরীক্ষা (test) করা এত সহজ কেন?

Pure function-এর দুটি শর্ত: (১) একই ইনপুটে সবসময় একই ফল, আর (২) বাইরের জগতে কোনো side effect নেই — স্ক্রিনে লেখা, ফাইলে লেখা, global ভ্যারিয়েবল বদলানো, নেটওয়ার্কে কিছু পাঠানো কিছুই না। এই পর্বের area(double r) বা factorial(int n) pure: area(2.0) আজ, কাল, যেকোনো কম্পিউটারে একই। কিন্তু একটি bill() যদি VAT হার পড়ে একটি global ভ্যারিয়েবল থেকে, আর ফল ছাপিয়েও দেয়, তাহলে তা pure নয় — একই argument-এ ভিন্ন দিনে ভিন্ন ফল আসতে পারে।

পরীক্ষা সহজ কেন: একটি pure ফাংশন পরীক্ষা করতে শুধু লাগে “এই ইনপুটে এই ফল আসা উচিত” — bill(200, 3, 0.15) হতে হবে 690. কোনো ফাইল তৈরি, ডেটাবেস সাজানো বা আগে-পরে কোন ফাংশন চলেছে তা মনে রাখার দরকার নেই। হাজারটা এমন পরীক্ষা কয়েক মিলিসেকেন্ডে চলে, যেকোনো ক্রমে, একসাথে। পর্ব ৪৮-এ unit test লেখার সময় দেখবেন, পরীক্ষা করা কঠিন কোড প্রায় সবসময় side effect-এ ভরা কোড।

বোঝা সহজ কেন: pure ফাংশন পড়তে শুধু তার নিজের কয়েকটি লাইন দেখলেই চলে। ফাংশন যদি global অবস্থা পড়ে বা বদলায়, তবে তার আচরণ বুঝতে পুরো প্রোগ্রামের ইতিহাস জানতে হয় — কে কখন সেই global বদলেছে।

নিরাপদ ও দ্রুত কেন: একাধিক thread (পর্ব ৪৫) একসাথে একটি pure ফাংশন ডাকলেও কোনো সংঘর্ষ নেই, কারণ কেউ ভাগের কিছু বদলাচ্ছে না। আর একই ইনপুটে একই ফল বলে আগের ফল মনে রেখে দেওয়া (memoization) নিরাপদ — fibonacci-কে কোটি ডাক থেকে কয়েক ডজনে নামিয়ে আনার কৌশল এটাই। Compiler-ও pure ফাংশনকে বেশি সাহসের সাথে optimize করতে পারে; GCC-তে এ জন্য __attribute__((pure)) আর const ইঙ্গিত আছে। C++-এর constexpr ফাংশনও স্বাভাবিকভাবেই এই দিকে ঠেলে দেয়, কারণ compile-এর সময় ফাইল পড়া বা স্ক্রিনে লেখা যায় না।

Haskell-এর মতো functional ভাষা পুরো প্রোগ্রামই এই নীতিতে গড়ে। বাস্তব C++ প্রোগ্রামে side effect লাগবেই — ফাইল সেভ না করলে, স্ক্রিনে না দেখালে প্রোগ্রাম কোনো কাজেরই নয়। কৌশল হলো side effect-কে প্রোগ্রামের কিনারায় ঠেলে দেওয়া: ভেতরের হিসাব pure, বাইরের পাতলা স্তরে ইনপুট-আউটপুট।

মূল উপলব্ধি: “ইনপুট parameter-এ, ফল return-এ” — এই পর্বের সহজ নিয়মটাই pure ফাংশনের ভিত্তি। হিসাব আর ইনপুট-আউটপুট আলাদা রাখুন; তাহলে কোড পরীক্ষা, বোঝা আর সমান্তরালে চালানো — তিনটিই সহজ হয়।

অনুশীলন

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

  1. কোন overload চলবে? শুধু দুটি ফাংশন আছে: void f(int) আর void f(double). প্রতিটি ডাকের জন্য বলুন কোনটি চলবে, নাকি error — (ক) f(7) (খ) f(7.0f) (গ) f(true) (ঘ) f(7u) (ঙ) f("7")

    (ক) f(int) — exact match. (খ) f(double) — float থেকে double promotion. (গ) f(int) — bool থেকে int promotion. (ঘ) ambiguous error — unsigned থেকে int আর double দুটোই conversion, সমান ধাপ। (ঙ) error — অক্ষরের array থেকে int বা double-এ কোনো পথ নেই, কোনো প্রার্থীই নেই।

  2. কোন declaration বৈধ? (ক) void a(int x, int y = 0, int z = 0); (খ) void b(int x = 0, int y); (গ) void c(int x = 0, int y = 0); আর পরে definition void c(int x = 0, int y = 0) { } (ঘ) int d(int x); আর double d(int x);

    (ক) বৈধ — default ডান দিক থেকে। (খ) অবৈধ — x-এ default আছে কিন্তু তার ডানের y-তে নেই। (গ) declaration বৈধ, কিন্তু definition-এ default আবার লেখায় error; definition-এ লিখুন void c(int x, int y) { }. (ঘ) অবৈধ — শুধু return টাইপ আলাদা, parameter-তালিকা এক; এটা overload নয়।

  3. ডাক গুনুন: fib(0) = 0, fib(1) = 1, আর বাকিদের জন্য fib(n) = fib(n-1) + fib(n-2) — এভাবে recursive লেখা হলে fib(4) হিসাব করতে মোট কতবার fib ডাকা হয়? stack-এর সর্বোচ্চ গভীরতা কত?

    মোট ৯ বার: fib(4) ডাকে fib(3) ও fib(2); fib(3) ডাকে fib(2) ও fib(1); প্রতিটি fib(2) ডাকে fib(1) ও fib(0). গুনলে ১ + ৫ (fib(3)-এর গাছ) + ৩ (fib(2)-এর গাছ) = ৯. উত্তর fib(4) = 3. সর্বোচ্চ গভীরতা ৪ frame (fib(4) → fib(3) → fib(2) → fib(1)), কারণ এক সময়ে stack-এ শুধু একটি শাখা থাকে। fib(2) দুবার হিসাব হলো — n বাড়লে এই পুনরাবৃত্তিই ডাকের সংখ্যাকে বিস্ফোরিত করে।

  4. নকশা ও কোড: overload.cpp-এ ত্রিভুজের ক্ষেত্রফলের জন্য double area(double base, double height) যোগ করতে চাইলেন। কী সমস্যা হবে? তারপর তিনটি int-এর বড়টি বের করার জন্য maxOf(int, int, int) লিখুন, যেটি দুই-argument সংস্করণকেই ব্যবহার করে।

    ত্রিভুজের area(double, double)-এর signature আয়তক্ষেত্রের সাথে হুবহু এক — parameter-এর নাম আলাদা হলেও টাইপের তালিকা এক, তাই compiler একে একই ফাংশনের দ্বিতীয় definition ধরে error দেয় (“redefinition”). সমাধান: আলাদা নাম (triangleArea), বা পরে নিজের টাইপ বানিয়ে (struct Triangle, পর্ব ১২) area(Triangle) — তখন টাইপই অর্থ বহন করে। তিন-argument সংস্করণ: int maxOf(int a, int b, int c) { return maxOf(maxOf(a, b), c); } — argument-এর সংখ্যা আলাদা বলে এটি বৈধ overload, আর ভেতরে দুই-argument সংস্করণ দুবার ডাকা হয়।

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

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

ক্রমC++-এ একটি ফাংশন ডাকার পর যা যা ঘটে, সেগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ডাকার জায়গায় argument-গুলোর মান হিসাব করা হয়
  2. call stack-এ নতুন stack frame তৈরি হয়, argument কপি হয়ে parameter-এ বসে
  3. ফাংশনের শরীর চলে, local ভ্যারিয়েবল জন্মায়
  4. return ফলটি ডাকার জায়গায় পাঠায়
  5. frame মুছে যায়, local ভ্যারিয়েবলের lifetime শেষ হয়

C++-এ নিচের কোন জোড়া declaration বৈধ function overloading নয়?

  • double area(double r); আর double area(double w, double h);
  • int maxOf(int a, int b); আর double maxOf(double a, double b);
  • int parse(std::string s); আর double parse(std::string s);
  • void print(int x); আর void print(std::string s);

C++-এ void setup(int a = 1, int b); declaration লিখলে কী হয়?

  • b নিজে থেকে 0 ধরে নেওয়া হয়
  • Compile error, কারণ a-এর ডানের b-তে default নেই
  • setup(5) ডাকলে 5 বসে b-তে, a হয় 1
  • Compile হয়, কিন্তু চলার সময় crash করে

শুধু void print(int) আর void print(double) থাকলে print('A') কোনটি ডাকে?

  • print(int), আর ছাপে 65
  • print(double), আর ছাপে 65.0
  • Ambiguous call — compile error
  • print(char), আর ছাপে A

শুধু void f(int) আর void f(double) থাকলে f(10L) (একটি long) ডাকলে কী হয়?

  • f(int) চলে, কারণ দুটোই পূর্ণসংখ্যা
  • f(double) চলে, কারণ double বড় টাইপ
  • চলার সময় যেকোনো একটি এলোমেলোভাবে চলে
  • Compile error — ambiguous call

void inc(int x) { x = x + 1; } থাকলে int a = 5; inc(a);-এর পরে a-এর মান কত?

  • 6
  • মান অনির্দিষ্ট (garbage)
  • 5
  • Compile error, কারণ void ফাংশন কিছু ফেরত দেয় না

আধুনিক C++-এ ফাংশনের আগে inline লেখার মূল কার্যকর অর্থ কোনটি?

  • compiler বাধ্য হয়ে ডাকের জায়গায় কোড বসাবে, তাই প্রোগ্রাম নিশ্চিতভাবে দ্রুত হবে
  • একই definition একাধিক .cpp ফাইলে থাকতে পারে (যেমন header-এ), যদি সব কপি হুবহু এক হয়
  • ফাংশনের ফল compile-এর সময়েই হিসাব হবে
  • ফাংশনটি শুধু একই ফাইল থেকে ডাকা যাবে

শব্দকোষ

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

মনে রাখুন

  1. ফাংশন একটি চুক্তি: return টাইপ, নাম, parameter. Declaration চুক্তি জানায়, definition কাজ করে — definition পুরো প্রোগ্রামে একবারই।
  2. ডিফল্টভাবে pass-by-value: ফাংশন কপি পায়। প্রতিটি ডাক নিজের stack frame পায়; return-এর সাথে local ভ্যারিয়েবল মুছে যায়, তাই তাদের reference ফেরত দেওয়া বিপদ।
  3. Default argument শুধু ডান দিক থেকে, শুধু declaration-এ; মাঝের argument বাদ দেওয়া যায় না।
  4. Overloading = এক নাম, ভিন্ন parameter-তালিকা; বাছাই হয় exact > promotion > conversion ক্রমে, সমান হলে ambiguous error. শুধু return টাইপে overload হয় না।
  5. আধুনিক অভ্যাস: গুরুত্বপূর্ণ ফলে [[nodiscard]]; inline মানে ODR-এর ছাড়, গতি নয়; recursion-এ সবসময় base case.

পরের পর্বে: এই পর্বে বারবার বলা হয়েছে “কপি যায়।” এবার কপির বদলে আসলটাই পাঠানোর উপায় — int& আর const std::string&. Reference কী, কেন pointer-এর চেয়ে নিরাপদ, আর local ভ্যারিয়েবলের reference ফেরত দেওয়া কেন বিপজ্জনক — সব পর্ব ১০-এ।

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

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