এই পর্বে যা শিখবেন
if,else if,elseদিয়ে সিদ্ধান্ত — আর nested শর্তের বদলে early return কেন পরিষ্কার- C++17-এর init-statement,
switch-এর fall-through,[[fallthrough]]আরdefault-এর গুরুত্ব while,do-while,forআর range-basedfor— কোন কাজে কোন লুপbreak,continue, loop invariant, off-by-one আর অসীম লুপ চেনা- nested লুপের খরচ, আর AI-এর লেখা লুপে সাধারণ ভুল ধরার চেকলিস্ট
১ · প্রোগ্রামের পথ — সোজা চলা, মোড় নেওয়া, ঘুরে আসা
এ পর্যন্ত আপনার প্রতিটি প্রোগ্রাম ওপর থেকে নিচে, একটির পর একটি লাইন চলেছে। কিন্তু বাস্তব কাজে এটুকু যথেষ্ট নয়। বিদ্যুৎ বিল কত হবে, তা নির্ভর করে কত ইউনিট খরচ হয়েছে তার ওপর। ক্লাসের ৬০ জন ছাত্রের নম্বর থেকে সর্বোচ্চ বের করতে একই কাজ ৬০ বার করতে হয়। প্রোগ্রাম কোন লাইন কখন, কতবার চালাবে — এই ক্রমকে বলে control flow.
ঢাকার একটি বাসের রুট ভাবুন। বাস বেশিরভাগ সময় সোজা চলে। কোনো মোড়ে এসে সিদ্ধান্ত নেয় — রাস্তা বন্ধ থাকলে বাঁয়ে, নইলে সোজা। আর দিনশেষে একই রুটে বারবার ঘুরে আসে, যতক্ষণ না শেষ ট্রিপের সময় হয়। প্রোগ্রামও ঠিক এই তিন রকম চলে:
➡️ Sequence
লাইনগুলো যেভাবে লেখা, সেভাবেই একটির পর একটি চলে। এতদিন আপনি এটুকুই দেখেছেন।
সোজা রাস্তা🔀 Selection
শর্ত দেখে কোনো একটি পথ বেছে নেওয়া — if, else, switch. একটি পথ চলে, বাকিগুলো বাদ।
🔁 Iteration
শর্ত যতক্ষণ সত্য, একই অংশ বারবার চালানো — while, for, range-for.
১৯৬৬ সালে Corrado Böhm আর Giuseppe Jacopini দেখান যে যেকোনো হিসাবযোগ্য কাজ শুধু এই তিনটি কাঠামো দিয়েই লেখা যায়। এই চিন্তা থেকেই আসে structured programming: প্রতিটি অংশের একটি প্রবেশপথ, একটি বের হওয়ার পথ, যেন কোড ওপর থেকে নিচে পড়ে বোঝা যায়। C++-এর if, switch আর লুপ সবই এই নকশার উত্তরাধিকার।
এই পর্বে দুটি বড় অংশ — সিদ্ধান্ত আর লুপ। দুটিই C-এর মতো, তাই এখানে আমরা মূল ধারণা আর আধুনিক C++-এর নতুন সুবিধাগুলোয় জোর দেব। প্রতিটি খুঁটিনাটি নিয়ে আরও গভীরে যেতে চাইলে C কোর্সের সংশ্লিষ্ট পর্বের লিংক পাবেন।
২ · if, else if, else — শর্তের সিঁড়ি
সবচেয়ে সরল সিদ্ধান্ত: “শর্ত সত্য হলে এই কাজটি করো।” C++-এ লেখা হয় if (শর্ত) { ... }. বন্ধনীর ভেতরের শর্তটি একটি bool মান দেয় — true হলে block-টি চলে, false হলে লাফিয়ে পরের অংশে যায়। সাথে else দিলে “নইলে অন্যটা করো।” আর একাধিক বিকল্প থাকলে else if দিয়ে একটি সিঁড়ি বানানো হয়। পুরো কাঠামোকে বলে if-else.
বাস্তব উদাহরণ নিই — বিদ্যুৎ বিলের ধাপভিত্তিক দাম (slab). বাংলাদেশের বিতরণ কোম্পানিগুলো এমন ধাপে দাম ধরে: যত বেশি খরচ, ওপরের ধাপের প্রতি ইউনিটের দাম তত বেশি। আসল হার সময়ে সময়ে বদলায়, তাই এখানে সহজ একটি কাল্পনিক হার নিচ্ছি:
| ধাপ | ইউনিট | প্রতি ইউনিট | এই ধাপ পুরো হলে |
|---|---|---|---|
| ১ | ১ – ৭৫ | ৫ টাকা | ৩৭৫ টাকা |
| ২ | ৭৬ – ২০০ | ৭ টাকা | ৮৭৫ টাকা (মোট ১২৫০) |
| ৩ | ২০১ – ৩০০ | ৮ টাকা | ৮০০ টাকা (মোট ২০৫০) |
| ৪ | ৩০০-এর বেশি | ১০ টাকা | বাকি সব ইউনিট |
২৫০ ইউনিট খরচ হলে প্রোগ্রাম কোন পথে যায়, ধাপে ধাপে দেখুন। খেয়াল করুন কোন শর্তগুলো পরীক্ষা হয়, আর কোনগুলো একদমই হয় না:
1int units = 250, bill{};2if (units <= 75) {3 bill = units * 5;4} else if (units <= 200) {5 bill = 375 + (units - 75) * 7;6} else if (units <= 300) {7 bill = 1250 + (units - 200) * 8;8} else {9 bill = 2050 + (units - 300) * 10;10}
| units | |
| bill |
- শুরু।
units-এ ২৫০,billখালি{}দিয়ে শূন্য (পর্ব ০৫-এর নিয়ম)। - প্রথম শর্ত মিথ্যা। ২৫০ কি ৭৫-এর সমান বা কম? না। তাই লাইন ৩ চলে না, প্রোগ্রাম লাফ দেয় পরের
else if-এ। - দ্বিতীয় শর্তও মিথ্যা। ২৫০ কি ২০০-এর সমান বা কম? না। লাইন ৫ বাদ।
- তৃতীয় শর্ত সত্য। ২৫০ ≤ ৩০০ — হ্যাঁ। এবার এই block-এ ঢোকা যাবে। খেয়াল করুন: এখানে
units > 200লেখার দরকার হয়নি, কারণ আগের শর্ত মিথ্যা হয়েছে বলেই আমরা এখানে এসেছি। সিঁড়ির ক্রমই অর্ধেক শর্ত বলে দেয়। - হিসাব। প্রথম দুই ধাপ পুরো (১২৫০ টাকা), আর তৃতীয় ধাপে বাকি ৫০ ইউনিট × ৮ = ৪০০. মোট ১৬৫০ টাকা।
- বাকিটা বাদ। একটি শাখা চলার পর পুরো সিঁড়ি শেষ —
else-এর ভেতরে আর যাওয়াই হয় না। নিয়ম: প্রথম সত্য শর্তটিই জেতে। তাই শর্তের ক্রম ভুল হলে (যেমন আগেunits <= 300লিখলে) ৫০ ইউনিটের গ্রাহকও তৃতীয় ধাপের দাম পেতেন।
এবার একসাথে চারজন গ্রাহকের বিল হিসাব করে দেখুন। এখানে একটি range-for লুপও আছে, যা অংশ ৬-এ বিস্তারিত দেখবেন — আপাতত শুধু জানুন, এটি তালিকার প্রতিটি মান একবার করে units-এ বসায়:
#include <print>
int main() {
constexpr int customers[] = {50, 150, 250, 400}; // units
for (int units : customers) {
int bill{};
if (units <= 75) {
bill = units * 5;
} else if (units <= 200) {
bill = 375 + (units - 75) * 7;
} else if (units <= 300) {
bill = 1250 + (units - 200) * 8;
} else {
bill = 2050 + (units - 300) * 10;
}
std::println("{:>4} units -> {:>5} taka", units, bill);
}
}আউটপুট চার লাইন: ৫০ ইউনিটে 250, ১৫০-এ 900, ২৫০-এ 1650, আর ৪০০-তে 3050 টাকা। প্রতিবার ঠিক একটি শাখা চলে। {:>4} মানে ডান দিকে সাজিয়ে ৪ ঘর জায়গা নেওয়া (পর্ব ০৭)। 375, 1250, 2050 সংখ্যাগুলো আসলে আগের ধাপগুলোর যোগফল — বাস্তব কোডে এগুলো constexpr নামযুক্ত ধ্রুবক হওয়া উচিত, যাতে হার বদলালে এক জায়গায় বদলানো যায়।
তিনটি পুরোনো ফাঁদ
if (x = 5): এটি তুলনা নয়, মান বসানো।x-এ 5 বসে, ফল 5 — যা সত্য, তাই শর্ত সবসময় সত্য। GCC-Wall-এ সতর্ক করে। তুলনা হলো==(পর্ব ০৬)।if (0 < x < 10): গণিতের মতো দেখালেও C++ এটিকে পড়ে(0 < x) < 10— প্রথম অংশtrue/falseঅর্থাৎ 1/0, যা সবসময় 10-এর কম। সঠিক রূপ0 < x && x < 10.- বন্ধনী ছাড়া
if:{}না দিলে শুধু পরের একটি statement শর্তের অধীন। পরে কেউ দ্বিতীয় লাইন যোগ করলে সেটি সবসময় চলবে। নিরাপদ অভ্যাস — একটি লাইন হলেও{}দিন।
Nested শর্ত বনাম early return
শর্তের ভেতরে শর্ত, তার ভেতরে আরও শর্ত — কোড ডানে সরতে সরতে একটি পিরামিড হয়ে যায়। পড়তে গেলে মাথায় রাখতে হয় এখন কোন কোন শর্ত সত্য। বিকল্প হলো guard clause: অস্বাভাবিক বা ভুল অবস্থাগুলো শুরুতেই আলাদা করে বের করে দেওয়া, যাতে মূল কাজটি সোজা লাইনে থাকে। নিচে একই ফাংশন দুইভাবে (ফাংশন লেখার নিয়ম পর্ব ০৯-এ বিস্তারিত):
int bill_for(int units, bool connected) {
if (connected) {
if (units >= 0) {
if (units <= 75) {
return units * 5;
} else {
return 375 + (units - 75) * 7; // সংক্ষেপে
}
} else {
return -1; // ভুল মিটার রিডিং
}
} else {
return 0; // সংযোগ নেই
}
}int bill_for(int units, bool connected) {
if (!connected) return 0; // সংযোগ নেই
if (units < 0) return -1; // ভুল মিটার রিডিং
if (units <= 75) return units * 5;
return 375 + (units - 75) * 7; // সংক্ষেপে
}দ্বিতীয়টিতে প্রতিটি লাইন একটি স্বাধীন কথা বলে: “সংযোগ না থাকলে শূন্য”, “রিডিং ঋণাত্মক হলে ভুল।” এর পরে পাঠক নিশ্চিত — সংযোগ আছে, রিডিং ঠিক। লুপের ভেতরে একই কাজ করে continue (অংশ ৭)।
৩ · C++17-এর উপহার — শর্তের সাথে ভ্যারিয়েবল বানানো
প্রায়ই এমন হয়: একটি মান বের করেন শুধু তা পরীক্ষা করার জন্য। যেমন একটি ইমেইল ঠিকানায় @ চিহ্ন কোথায় আছে খোঁজা। std::string-এর find অবস্থান ফেরত দেয়, আর না পেলে দেয় একটি বিশেষ মান std::string::npos. পুরোনো লেখায়:
auto pos = email.find('@'); // pos এখন পুরো ফাংশনে দৃশ্যমান
if (pos != std::string::npos) {
std::println("user = {}", email.substr(0, pos));
}
// ... ৪০ লাইন পরে কেউ ভুল করে আবার pos ব্যবহার করলেনসমস্যা হলো pos-এর জীবন। এর কাজ শেষ if-এর সাথেই, কিন্তু সে বেঁচে থাকে পুরো ফাংশন জুড়ে। পরে কেউ অন্য খোঁজের জন্য আবার pos নাম নিতে চাইলে সংঘর্ষ, বা পুরোনো মানটি ভুল করে ব্যবহার। C++17 থেকে if-এর ভেতরেই একটি init-statement লেখা যায়:
if (auto pos = email.find('@'); pos != std::string::npos) {
std::println("user = {}", email.substr(0, pos));
} else {
std::println("invalid email"); // pos এখানেও দেখা যায়
}
// এখানে pos আর নেই — compiler-ই আটকে দেবেসেমিকোলনের আগের অংশ ভ্যারিয়েবল বানায়, পরের অংশ শর্ত। ভ্যারিয়েবলটির scope — অর্থাৎ কোডের যে অংশে নামটি চেনা যায় — if আর তার else পর্যন্তই। এটি for (int i = 0; ...)-এর মতোই, যেখানে i লুপের বাইরে থাকে না। একই সুবিধা switch-এও আছে: switch (auto c = read_choice(); c) { ... }.
পরীক্ষার হলে প্রশ্নপত্রের সাথে একটি rough কাগজ দেওয়া হয়। হিসাব শেষ হলে সেটি জমা দিতে হয়, বাড়ি নিয়ে যাওয়া যায় না। init-statement-এর ভ্যারিয়েবল সেই rough কাগজ — যে কাজের জন্য দেওয়া, সেই কাজ শেষ হলেই ফেরত। নিয়ম: একটি নামের scope যত ছোট, ভুলের জায়গা তত কম।
if constexpr — একটি নাম শুধু চিনে রাখুন
C++17-এ আরেকটি রূপ এসেছে: if constexpr. সাধারণ if-এর শর্ত প্রোগ্রাম চলার সময় পরীক্ষা হয়। if constexpr-এর শর্ত পরীক্ষা হয় compile-এর সময়, আর মিথ্যা শাখাটি template-এর ক্ষেত্রে compile-ই হয় না। এর আসল কাজ template-এ — “টাইপ সংখ্যা হলে এভাবে, লেখা হলে ওভাবে।” পর্ব ৩৯-এ constexpr-এর সাথে এটি বিস্তারিত দেখবেন।
৪ · switch — একটি মান, অনেক দরজা
যখন একটি মাত্র পূর্ণসংখ্যা বা char-এর ভিন্ন ভিন্ন মানের জন্য ভিন্ন কাজ, তখন লম্বা else if-এর চেয়ে switch পরিষ্কার। ধরুন ১০০ নম্বরের পরীক্ষায় গ্রেড বের করবেন। marks / 10 (integer division) দিলে ৮৭ নম্বর হয় 8, ৯৪ হয় 9, ১০০ হয় 10:
switch (marks / 10) {
case 10:
case 9:
case 8: grade = 'A'; break;
case 7: grade = 'B'; break;
case 6: grade = 'C'; break;
case 5: grade = 'D'; break;
default: grade = 'F'; break; // বাকি সব মান
}প্রতিটি case আসলে একটি দরজার নম্বর। মান যে দরজার সাথে মেলে, প্রোগ্রাম সেখান থেকে চলা শুরু করে — আর থামে break পেলে। কোনো দরজা না মিললে যায় default-এ। এখানে case 10:, case 9: খালি — তাই ১০ বা ৯ এলে সে নিচে গড়িয়ে case 8-এর কাজটিই করে। একে বলে fall-through.
Fall-through — সুবিধা, আর বিখ্যাত বাগ
খালি case-এর গড়িয়ে পড়া কাজের জিনিস। কিন্তু কাজসহ একটি case-এর শেষে break ভুলে গেলে সেটাও গড়িয়ে পরের case-এর কাজ করে ফেলে — নিঃশব্দে।
switch (level) {
case 1: discount = 5; // break ভুলে গেছি!
case 2: discount = 10; break; // level 1 হলেও discount হয় 10
case 3: bonus = 50;
[[fallthrough]]; // ইচ্ছাকৃত: level 3-এ 4-এর সুবিধাও
case 4: discount = 20; break;
default: discount = 0; break;
}কোনটা ভুল আর কোনটা ইচ্ছাকৃত, পাঠক বুঝবেন কীভাবে? C++17-এ এসেছে [[fallthrough]] — একটি attribute, যা compiler আর পাঠক দুজনকে বলে “এখানে break না থাকাটা আমার সিদ্ধান্ত।” GCC আর Clang-এ -Wimplicit-fallthrough (GCC-তে -Wextra-র অংশ) চালু থাকলে case 1-এর লাইনে সতর্কবার্তা আসবে, কিন্তু case 3-এ আসবে না। ফলে আসল বাগটি আলাদা হয়ে চোখে পড়ে।
switch-এর নিয়ম ও সীমা
- শুধু পূর্ণসংখ্যা-জাতীয় মান:
int,char,enum.std::stringবাdoubleদিয়েswitchহয় না — সেখানেif/else ifলাগবে। - case-এর মান compile-এর সময় জানা ধ্রুবক:
case 5:বাcase MAX:(যদিMAXconstexprহয়)।case x:যেখানেxসাধারণ ভ্যারিয়েবল — চলবে না।case 1 ... 5-এর মতো পরিসরও standard C++-এ নেই। - case-এর ভেতরে ভ্যারিয়েবল বানালে
{}দিন: নইলে compiler বলে “jump to case label crosses initialization”, কারণ পরের case-এ লাফ দিলে ভ্যারিয়েবলটির initialization বাদ পড়ে যেত। - সবসময়
default: “এমন মান তো আসবেই না” — এই বিশ্বাসই সবচেয়ে বেশি বাগ আনে। অন্তত একটি বার্তা ছাপান বা error দিন।
enum-এর সাথে switch — compiler-কে পাহারায় বসানো
switch-এর সবচেয়ে শক্তিশালী ব্যবহার enum class-এর সাথে, যা পর্ব ১২-এ শিখবেন। ছোট একটি ঝলক:
enum class Payment { Cash, Card, Bkash, Nagad };
switch (method) {
case Payment::Cash: fee = 0; break;
case Payment::Card: fee = 15; break;
case Payment::Bkash: fee = 18; break;
// Nagad ভুলে গেছি — g++ -Wall বলবে:
// warning: enumeration value 'Nagad' not handled in switch
}এখানে default না রাখাটা ইচ্ছাকৃত কৌশল। default থাকলে compiler ধরে নেয় বাকিগুলো আপনি সামলেছেন, আর নতুন একটি মান (যেমন পরে Rocket যোগ হলো) যোগ করলে কোনো সতর্কবার্তা আসে না। default না থাকলে -Wswitch প্রতিটি অসম্পূর্ণ switch দেখিয়ে দেয়। তাই নিয়মটা দাঁড়ায়: সাধারণ সংখ্যায় সবসময় default; নিজের enum-এ সব মান লিখুন, আর অজানা মান সামলান switch-এর পরে।
৫ · লুপ — while, do-while আর for
একই কাজ বারবার করার কাঠামোকে বলে loop, আর প্রতিবার একবার চালানোকে বলে একটি iteration. C++-এ মূল লুপ তিনটি, আর তিনটির পার্থক্য একটাই প্রশ্নে — শর্ত কখন পরীক্ষা হয়, আর গোনার হিসাব কোথায় লেখা থাকে।
| লুপ | শর্ত কখন | ন্যূনতম কতবার চলে | কখন বেছে নেবেন |
|---|---|---|---|
| while | প্রতিবার শুরুর আগে | ০ বার | কতবার চলবে আগে জানা নেই — “ফাইল শেষ না হওয়া পর্যন্ত পড়ো” |
| do-while | প্রতিবার শেষে | ১ বার | কাজটি অন্তত একবার হতেই হবে — মেনু দেখানো, ইনপুট চাওয়া |
| for | প্রতিবার শুরুর আগে | ০ বার | গোনা জানা — “১ থেকে ১০”, সূচক দিয়ে কাজ |
| range-for | container শেষ হলো কি না | ০ বার | container-এর প্রতিটি উপাদানে একবার — সবচেয়ে প্রচলিত |
for আসলে while-এরই গোছানো রূপ। for (শুরু; শর্ত; পরিবর্তন) { কাজ } — তিনটি অংশ এক লাইনে, যেন গোনার পুরো হিসাব এক জায়গায় দেখা যায়। ১ থেকে ৪ যোগ করার লুপটি ধাপে ধাপে চালিয়ে দেখুন। প্রতিটি ঘর নিজে আগে অনুমান করুন:
1int sum{};2for (int i = 1; i <= 4; ++i) {3 sum += i;4}5std::println("{}", sum);
| i | |
| i <= 4 ? | |
| sum | |
| iteration |
- লুপের আগে।
sumশূন্য দিয়ে শুরু। এটি না দিলে যোগ শুরু হতো আবর্জনা মান থেকে — লুপের সবচেয়ে পুরোনো বাগ। - শুরুর অংশ একবারই চলে।
int i = 1— পুরো লুপে মাত্র একবার। তারপর শর্ত: ১ ≤ ৪, সত্য। ভেতরে ঢোকা যাবে। - ১ম iteration.
sum += i→ ০ + ১ = ১. Block শেষ হলে চলে তৃতীয় অংশ++i,iহয় ২. তারপর আবার শর্ত। - ২য় iteration. ২ ≤ ৪, সত্য।
sum= ১ + ২ = ৩. ক্রমটি খেয়াল করুন: শর্ত → কাজ → পরিবর্তন → শর্ত … - ৩য় iteration.
sum= ৩ + ৩ = ৬. প্রতিটি ধাপের শেষে একটি কথা সবসময় সত্য: “sumহলো ১ থেকেiপর্যন্ত যোগফল।” এমন কথাকে বলে loop invariant — অংশ ৭-এ এর কাজ দেখবেন। - ৪র্থ iteration.
sum= ৬ + ৪ = ১০. তারপর++i,iহলো ৫. - শর্ত মিথ্যা, লুপ শেষ। ৫ ≤ ৪ মিথ্যা। মোট ৪ বার কাজ হলো, কিন্তু শর্ত পরীক্ষা হলো ৫ বার। লুপ থেকে বের হলে
iআর নেই — সেfor-এর scope-এ জন্মেছিল। আউটপুট 10.
do-while — অন্তত একবার: মেনু লুপ
একটি মোবাইল রিচার্জের মেনু ভাবুন। ব্যবহারকারী বের হতে চাইবেন কি না, তা জানতে হলে আগে অন্তত একবার মেনু দেখাতে হবে। এটিই do-while-এর স্বাভাবিক কাজ (এটি std::cin থেকে ইনপুট নেয়, তাই এখানে চালানোর বোতাম নেই):
int choice{};
do {
std::println("1) Balance 2) Recharge 0) Exit");
if (!(std::cin >> choice)) break; // ইনপুট শেষ বা ভুল হলে বের হও
switch (choice) {
case 1: std::println("Balance: 120 taka"); break;
case 2: std::println("Recharge done"); break;
case 0: std::println("Bye"); break;
default: std::println("Wrong choice"); break;
}
} while (choice != 0);শেষের সেমিকোলনটি খেয়াল করুন — while (choice != 0);. do-while-এ এটি বাধ্যতামূলক। আর std::cin ব্যর্থ হলে (কেউ অক্ষর লিখলেন, বা ইনপুট শেষ) break না থাকলে এই লুপ চিরকাল ঘুরত, কারণ ব্যর্থ stream আর নতুন মান পড়ে না (পর্ব ০৭)।
অসীম লুপ — কখন বাগ, কখন নকশা
যে লুপের শর্ত কখনো মিথ্যা হয় না, তা infinite loop. ভুল করে হয় সাধারণত তিনভাবে: পরিবর্তনের অংশ ভুলে যাওয়া (while (i < n), কিন্তু ++i নেই), ভুল দিকে বদলানো (--i যেখানে ++i দরকার), বা এমন শর্ত যা কখনো মিথ্যা হয় না (অংশ ৭-এর unsigned ফাঁদ)। আবার ইচ্ছা করেও অসীম লুপ লেখা হয় — গেমের মূল লুপ, সার্ভার যা সারাদিন অনুরোধ শোনে, ATM-এর সফটওয়্যার। সেখানে লেখা হয় while (true) বা for (;;), আর ভেতরে স্পষ্ট একটি break বা return থাকে।
প্রতিটি লুপ লেখার সময় নিজেকে একটি প্রশ্ন করুন: “প্রতিটি iteration কোন মানকে শেষের দিকে এক ধাপ এগিয়ে নেয়?” উত্তর দিতে না পারলে লুপটি হয়তো কখনো থামবে না। চলতে থাকা প্রোগ্রাম থামাতে টার্মিনালে Ctrl+C চাপুন।
if, switch, while, do-while, for, break, continue — সব C-এর মতো হুবহু এক। C++ যোগ করেছে চারটি জিনিস: শর্তে সত্যিকারের bool, C++17-এর if/switch init-statement, [[fallthrough]] আর container-এর জন্য range-for. প্রতিটি লুপের আরও উদাহরণ আর খুঁটিনাটি চাইলে C কোর্সের সিদ্ধান্ত — if আর switch আর লুপ পর্ব দুটি দেখুন।
৬ · Range-based for — সূচক ছাড়া প্রতিটি উপাদান
ক্লাসের সব নম্বর একটি std::vector-এ আছে (পর্ব ১৩-এ বিস্তারিত)। প্রতিটি নম্বর একবার করে দেখতে হবে। পুরোনো পথে সূচক দিয়ে: for (std::size_t i = 0; i < marks.size(); ++i) — তিনটি জায়গায় ভুলের সুযোগ: শুরু, শর্ত আর বৃদ্ধি। C++11 থেকে লেখা যায় শুধু:
for (const auto& m : marks) { // "marks-এর প্রতিটি m-এর জন্য"
std::println("{}", m);
}এটি array, std::vector, std::string (প্রতিটি অক্ষর) — যার শুরু আর শেষ আছে এমন সবকিছুর ওপর চলে। ভেতরে compiler এটিকে iterator দিয়ে লেখা একটি সাধারণ লুপে বদলে নেয় (পর্ব ৩২)। আপনার কাজ শুধু একটি সিদ্ধান্ত: লুপের ভ্যারিয়েবলটি কপি হবে, না আসল উপাদানের আরেক নাম?
| লেখা | x আসলে কী | কখন |
|---|---|---|
for (auto x : v) | প্রতিটি উপাদানের একটি কপি | ছোট টাইপ (int, char), যখন কপি বদলাতে চান |
for (auto& x : v) | আসল উপাদানের reference — অন্য নাম | উপাদান বদলাতে চাইলে |
for (const auto& x : v) | আসলটির reference, কিন্তু শুধু পড়া যায় | শুধু পড়া — ডিফল্ট পছন্দ, বিশেষ করে std::string বা বড় object-এ |
কপি আর reference-এর পার্থক্যটাই সবচেয়ে বেশি ভুল করায়। ছাত্রদের নম্বরে ৫ নম্বর grace দিতে গিয়ে কী হয়, ধাপে ধাপে দেখুন:
1std::vector<int> v{40, 55, 70};2for (auto x : v) x += 5;3// v এখন?4for (auto& x : v) x += 5;5// v এখন?
| v[0] | |
| v[1] | |
| v[2] | |
| x |
- শুরু। তিনজন ছাত্রের নম্বর ৪০, ৫৫, ৭০. লক্ষ্য — সবাইকে ৫ নম্বর বেশি দেওয়া।
auto x— কপি তৈরি। প্রথম iteration-এxএকটি নতুন, আলাদা বাক্স, যাতেv[0]-এর মান 40 কপি হলো।x += 5করলেxহয় 45 — কিন্তুv[0]এখনো 40.- প্রতিবার একই ঘটনা। দ্বিতীয় iteration-এ পুরোনো কপি ফেলে দিয়ে নতুন কপি (55), সেটা 60 হলো, আবার ফেলে দেওয়া হলো। ফটোকপিতে কলম চালালে মূল কাগজ বদলায় না।
- ফল — কিছুই বদলায়নি। লুপ শেষে
vআগের মতোই {40, 55, 70}. Compiler কোনো error দেয়নি, কারণ কোডটি বৈধ — শুধু আপনার উদ্দেশ্যের সাথে মেলে না। এটি AI-এর লেখা কোডেও খুব সাধারণ ভুল। auto&— একই বাক্সের অন্য নাম। এবারxনতুন বাক্স নয়,v[0]-এরই আরেকটি নাম (reference, পর্ব ১০-এ বিস্তারিত)।x += 5মানে সরাসরিv[0] += 5.- ফল — সবার নম্বর বাড়ল।
v= {45, 60, 75}. নিয়ম মনে রাখুন: বদলাতে চাইলেauto&, শুধু পড়তেconst auto&, আর ছোট টাইপের কপি দরকার হলেauto. বড়std::string-এর ওপরautoদিলে বাগ না হলেও প্রতিবার পুরো লেখার কপি হয় — অকারণ খরচ।
এবার সব একসাথে: range-for দিয়ে সর্বোচ্চ নম্বর খোঁজা, auto& দিয়ে grace দেওয়া, আর switch দিয়ে গ্রেড:
#include <print>
#include <vector>
int main() {
std::vector<int> marks{72, 45, 88, 91, 67};
int best = marks[0];
for (const auto& m : marks) {
if (m > best) best = m;
}
std::println("max = {}", best);
for (auto& m : marks) m += 5; // grace: vector bodlay
for (const auto& m : marks) {
char grade{};
switch (m / 10) {
case 10: case 9: case 8: grade = 'A'; break;
case 7: grade = 'B'; break;
case 6: grade = 'C'; break;
default: grade = 'F'; break;
}
std::print("{}:{} ", m, grade);
}
std::println("");
}প্রথম লাইনে max = 91. খেয়াল করুন, best শুরু হয়েছে প্রথম উপাদান দিয়ে, 0 দিয়ে নয় — সব নম্বর ঋণাত্মক হলেও (যেমন তাপমাত্রা) এটি ঠিক কাজ করবে। তারপর auto& লুপ সবার নম্বর ৫ বাড়ায়, তাই দ্বিতীয় লাইনে আসে 77:B 50:F 93:A 96:A 72:B. 50 পেয়েও F কেন? কারণ case 5 লেখা হয়নি, তাই 5 গেছে default-এ — switch-এ যা লেখেননি, তা সবই default. একটি সতর্কতা: marks খালি হলে marks[0] undefined behavior; বাস্তব কোডে আগে marks.empty() পরীক্ষা করুন।
Range-for-এ দুটি সাবধানতা, আর আধুনিক কিছু রূপ
- লুপের ভেতরে container-এর আকার বদলাবেন না।
for (auto& x : v) v.push_back(x);—push_backমেমরি নতুন জায়গায় সরাতে পারে, তখন লুপের ভেতরের iterator পুরোনো ঠিকানা ধরে থাকে: undefined behavior (পর্ব ১৮)। - সূচকও দরকার হলে: C++20 থেকে range-for-এও init-statement চলে —
for (std::size_t i = 0; const auto& m : marks) { ...; ++i; }. - সংখ্যার পরিসরের ওপর: C++20-এর std::views::iota দিয়ে
for (int i : std::views::iota(1, 11))লিখলেiচলে ১ থেকে ১০ পর্যন্ত (শেষ মানটি বাদ)। Ranges আর views-এর পুরো গল্প পর্ব ৩৫-এ।
৭ · break, continue আর off-by-one
লুপের মাঝখান থেকে পথ বদলানোর দুটি শব্দ আছে। break — “লুপ এখনই শেষ, বের হয়ে যাও।” continue — “এই iteration-এর বাকিটা বাদ, পরেরটায় যাও।” একটি বোলারের ওভারের রান যোগ করার কাল্পনিক নিয়ম দেখুন: ঋণাত্মক মান মানে ভুল এন্ট্রি (বাদ), আর 0 মানে ডেটার শেষ চিহ্ন (থামো):
1int runs[] = {5, -1, 3, 0, 7}; int sum{};2for (int r : runs) {3 if (r < 0) continue;4 if (r == 0) break;5 sum += r;6}7std::println("{}", sum);
| r | |
| sum |
- শুরু। পাঁচটি মান,
sumশূন্য। - 5 — স্বাভাবিক পথ। ঋণাত্মক নয়, শূন্যও নয়, তাই সোজা নিচে নেমে যোগ:
sum= 5. - -1 —
continue. শর্ত সত্য, তাই এই iteration-এর বাকি অংশ (লাইন ৪-৫) বাদ দিয়ে লুপ সরাসরি পরের মান নেয়। এটাই লুপের guard clause — ভুল ডেটা শুরুতেই সরিয়ে দেওয়া। - 3 — আবার যোগ।
sum= 8. - 0 —
break. লুপ এখানেই শেষ। শেষের 7 কখনো দেখাই হয় না। একটি বিশেষ মান দিয়ে ডেটার শেষ বোঝানোর এই কৌশলকে বলে sentinel. - ফল 8.
breakশুধু সবচেয়ে ভেতরের লুপ বাswitchথেকে বের করে। Nested লুপের দুই স্তর থেকে একসাথে বের হতে C++-এ (gotoছাড়া) সরাসরি উপায় নেই — সেজন্য ভেতরের অংশ একটি ফাংশনে রেখেreturnকরাই পরিষ্কার পথ (পর্ব ০৯)।
switch-এর ভেতরের break শুধু switch থেকে বের করে, চারপাশের লুপ থেকে নয়। মেনু লুপে case 0: break; লিখে ভাবলেন লুপ শেষ হবে — হবে না। সেজন্যই আগের মেনুতে লুপের শর্ত ছিল while (choice != 0).
Off-by-one — একঘর বেশি, একঘর কম
লুপের সবচেয়ে প্রচলিত ভুলের নাম off-by-one: একবার বেশি বা একবার কম চলা। ১০০ মিটার রাস্তায় প্রতি ১০ মিটারে একটি খুঁটি বসাতে কয়টি খুঁটি লাগে? ১০ নয়, ১১ — এই “fencepost” প্রশ্নই off-by-one-এর মূল। কোডে এটি সাধারণত তিন জায়গায় লুকায়:
| লেখা | সমস্যা | সঠিক রূপ |
|---|---|---|
for (int i = 0; i <= n; ++i) a[i] | n আকারের array-তে শেষ সূচক n-1; a[n] সীমার বাইরে — undefined behavior | i < n |
for (int i = 1; i < 10; ++i) — “১ থেকে ১০” | ১০ বাদ পড়ে, চলে ৯ বার | i <= 10, বা 0 থেকে < 10 |
for (std::size_t i = n - 1; i >= 0; --i) | unsigned কখনো ঋণাত্মক হয় না; 0-এর পর ঘুরে বিশাল সংখ্যা — অসীম লুপ | for (std::size_t i = n; i-- > 0; ), বা range দিয়ে উল্টো দিকে |
সবচেয়ে নিরাপদ অভ্যাস হলো C++-এর প্রচলিত half-open নিয়ম: শুরু ধরা, শেষ বাদ — [0, n), অর্থাৎ i = 0; i < n. এতে iteration সংখ্যা ঠিক n, খালি পরিসর মানে 0 < 0 মিথ্যা — লুপ নিজে থেকেই চলে না। Standard library-র প্রতিটি পরিসর (begin থেকে end) এই নিয়মে বানানো। আর container-এর ওপর হলে সবচেয়ে ভালো — সূচকই রাখবেন না, range-for নিন।
Signed-unsigned তুলনা — AI-এর প্রিয় সতর্কবার্তা
v.size() ফেরত দেয় std::size_t, একটি unsigned টাইপ। তাই for (int i = 0; i < v.size(); ++i) লিখলে g++ -Wall-এ বলে comparison of integer expressions of different signedness. ক্ষতিটা বাস্তব: ঋণাত্মক int unsigned-এ বদলে বিশাল সংখ্যা হয়ে যায়, ফলে -1 < v.size() মিথ্যা দাঁড়ায়। সমাধান তিনটি: সূচকের টাইপ std::size_t নিন, বা C++20-এর std::ssize(v) দিয়ে signed আকার নিন, বা সবচেয়ে ভালো — range-for.
Loop invariant — লুপ ঠিক কি না, প্রমাণের হাতিয়ার
অংশ ৫-এর trace table-এ একটি বাক্য প্রতিটি ধাপে সত্য ছিল: “sum হলো ১ থেকে i পর্যন্ত যোগফল।” লুপ লেখার সময় এমন একটি বাক্য খুঁজে নিন। সর্বোচ্চ খোঁজার লুপে invariant: “best হলো এ পর্যন্ত দেখা উপাদানগুলোর সর্বোচ্চ।” তিনটি প্রশ্ন করলেই লুপের সঠিকতা যাচাই হয় — শুরুতে কি এটি সত্য? প্রতিটি iteration কি একে সত্য রাখে? লুপ শেষে এটি কি ঠিক আপনার চাওয়া উত্তর দেয়? Off-by-one ভুল সাধারণত এই তিন প্রশ্নের প্রথম বা শেষটিতেই ধরা পড়ে।
৮ · লুপের ভেতরে লুপ — আর তার খরচ
লুপের ভেতরে আরেকটি লুপকে বলে nested loop. ছোটবেলার নামতার ছক এর সহজ উদাহরণ:
for (int row = 1; row <= 9; ++row) {
for (int col = 1; col <= 9; ++col) {
std::print("{:3}", row * col);
}
std::println("");
}বাইরের লুপ ৯ বার, প্রতিবার ভেতরের লুপ ৯ বার — মোট ৮১টি গুণ। এখন ৯-এর বদলে যদি n হয়? কাজ হয় n × n. এই বৃদ্ধির হারকে বলে O(n²) — Big-O notation-এ।
| n | একটি লুপ (n) | দুই স্তর (n²) | সেকেন্ডে প্রায় ১০ কোটি সরল কাজ ধরলে |
|---|---|---|---|
| ১০০ | ১০০ | ১০,০০০ | চোখের পলকে |
| ১০,০০০ | ১০,০০০ | ১০ কোটি | প্রায় ১ সেকেন্ড |
| ১০ লাখ | ১০ লাখ | ১০১২ | কয়েক ঘণ্টা |
ধরুন একটি মোবাইল ব্যাংকিং অ্যাপ ১০ লাখ লেনদেনের মধ্যে একই নম্বরের দুটি লেনদেন খুঁজছে। “প্রতিটির সাথে প্রতিটির তুলনা” — দুই স্তরের লুপ — ছোট পরীক্ষায় দিব্যি চলে, কিন্তু আসল ডেটায় ঘণ্টার পর ঘণ্টা লাগে। একই কাজ sort করে বা hash table দিয়ে (পর্ব ৩১, ৩৩) অনেক কম সময়ে হয়। তাই nested লুপ দেখলেই একবার জিজ্ঞেস করুন: n বড় হলে কী হবে? বিস্তারিত হিসাব শিখতে C কোর্সের Big-O ও সময়ের জটিলতা পর্বটি দেখুন।
whileআরdo-while-এর পার্থক্য? —whileআগে শর্ত দেখে (০ বারও চলতে পারে);do-whileপরে দেখে, তাই অন্তত একবার চলে।switch-এbreakনা দিলে কী হয়? — পরের case-এর কাজও চলে (fall-through); ইচ্ছাকৃত হলে C++17-এ[[fallthrough]];লেখা হয়।std::stringদিয়েswitchকরা যায়? — না; case-এর মান পূর্ণসংখ্যা-জাতীয় compile-time ধ্রুবক হতে হয়।for (auto x : v)আরfor (auto& x : v)-এর পার্থক্য? — প্রথমটি প্রতিটি উপাদানের কপি নেয়, বদলালেvবদলায় না; দ্বিতীয়টি আসল উপাদানের reference.for (int i = 0; i < 5; i++);লুপের পরের লাইন কতবার চলে? — একবার; শেষের সেমিকোলন নিজেই একটি খালি লুপ-দেহ।
৯ · AI-এর যুগে — লুপ AI লেখে, সীমানা পরীক্ষা আপনার
লুপ আর শর্ত AI টুল প্রায় নিখুঁতভাবে লেখে — দেখতে। কিন্তু লুপের বাগ থাকে সীমানায়: প্রথম উপাদান, শেষ উপাদান, খালি তালিকা, একটিমাত্র উপাদান। সাধারণ পরীক্ষায় এগুলো ধরা পড়ে না। AI-এর লেখা C++ লুপে বারবার যা দেখা যায়:
| AI যা লেখে | সমস্যা | যা হওয়া উচিত |
|---|---|---|
for (int i = 0; i < v.size(); i++) | signed-unsigned তুলনার সতর্কবার্তা; বড় বা ঋণাত্মক মানে ভুল | range-for, বা std::size_t / std::ssize |
i <= v.size() | off-by-one — শেষে সীমার বাইরে পড়া | i < v.size() |
for (auto s : names) (std::string) | প্রতিবার পুরো লেখার কপি; বদলালে আসলটি বদলায় না | পড়তে const auto&, বদলাতে auto& |
default ছাড়া switch (সংখ্যায়) | অপ্রত্যাশিত মান নিঃশব্দে উপেক্ষিত | default-এ বার্তা বা error |
কাজসহ case-এ break নেই | লুকানো fall-through | break, বা ইচ্ছাকৃত হলে [[fallthrough]] |
int maxv = 0; দিয়ে সর্বোচ্চ খোঁজা | সব মান ঋণাত্মক হলে উত্তর ভুল 0 | প্রথম উপাদান দিয়ে শুরু (খালি কি না আগে দেখে), বা std::max_element (পর্ব ৩৩) |
AI-কে লুপ লিখতে দেওয়ার সময় নিয়ম আর সীমানার পরীক্ষা দুটোই চেয়ে নিন:
AI-এর লেখা লুপ যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১১৯৬৮ সালে Dijkstra কেন বললেন goto “ক্ষতিকর”? C++-এ এখনো goto আছে — তাহলে কি সেটা ব্যবহার করা যায়?
প্রথম দিকের প্রোগ্রামিং ভাষায় (assembly, প্রথম যুগের FORTRAN, BASIC) control flow-এর প্রধান হাতিয়ার ছিল goto — “এই লেবেলে লাফ দাও।” লুপ, শর্ত, সবকিছু লাফ দিয়ে বানানো হতো। ছোট প্রোগ্রামে এটা চলত, কিন্তু হাজার লাইনের প্রোগ্রামে লাফগুলো এমনভাবে জট পাকাত যে কোড পড়া হয়ে যেত সুতোর জট খোলার মতো। এর নাম হয়েছিল “spaghetti code”.
১৯৬৮ সালের মার্চে Edsger W. Dijkstra-র একটি ছোট চিঠি ছাপা হয় Communications of the ACM পত্রিকায়, শিরোনাম “Go To Statement Considered Harmful”. মজার তথ্য — শিরোনামটি দিয়েছিলেন পত্রিকার সম্পাদক Niklaus Wirth (পরে Pascal ভাষার স্রষ্টা), Dijkstra নিজে নন। Dijkstra-র মূল যুক্তি ছিল মানুষের বোঝার সীমা নিয়ে: আমরা স্থির লেখা ভালো বুঝি, কিন্তু চলমান প্রক্রিয়া কল্পনা করতে কষ্ট হয়। তাই কোডের লেখা আর তার চলার ক্রম যত কাছাকাছি, প্রোগ্রাম তত বোঝা যায়। goto এই মিল ভেঙে দেয় — কোনো লাইনে কীভাবে পৌঁছানো গেল, তা জানতে পুরো প্রোগ্রাম খুঁজতে হয়।
এর আগে ১৯৬৬ সালেই Böhm ও Jacopini দেখিয়েছিলেন যে sequence, selection আর iteration দিয়েই যেকোনো হিসাব লেখা যায় — অর্থাৎ goto তাত্ত্বিকভাবে অপ্রয়োজনীয়। এই দুই কাজ মিলে ১৯৭০-এর দশকে structured programming আন্দোলন জোরালো হয়, আর Pascal, C-এর মতো ভাষা if, while, for-কে প্রধান করে তোলে।
বিতর্ক অবশ্য একতরফা ছিল না। ১৯৭৪ সালে Donald Knuth লেখেন “Structured Programming with go to Statements” — দেখান যে কিছু ক্ষেত্রে (যেমন গভীর nested লুপ থেকে বের হওয়া, ভুল হলে পরিষ্কার করা) সংযত goto কোডকে বরং সহজ করে। Linux kernel-এর C কোডে আজও ভুল সামলানোর জন্য goto cleanup; ধাঁচ প্রচলিত।
C++-এ goto আছে C-এর সাথে সামঞ্জস্যের জন্য, কিন্তু এর প্রয়োজন প্রায় নেই। C-তে যে “cleanup” কাজের জন্য goto লাগে, C++-এ তা করে destructor আর RAII (পর্ব ১৭) — scope শেষ হলে নিজে থেকেই। Nested লুপ থেকে বের হতে ফাংশন আর return. আর goto দিয়ে কোনো ভ্যারিয়েবলের initialization ডিঙিয়ে লাফ দেওয়া C++-এ compile error.
মূল উপলব্ধি: structured programming-এর আসল শিক্ষা “goto নিষিদ্ধ” নয়, বরং “কোডের আকার যেন তার চলার পথ দেখায়।” Early return, break, guard clause — সবই এই নীতির ভেতরে থেকে পথ ছোট করার সংযত উপায়।
প্র ০২switch কেন নিজে থেকে পরের case-এ গড়িয়ে পড়ে? এমন ঝুঁকিপূর্ণ নকশা কেন রাখা হলো, আর [[fallthrough]] কীভাবে সাহায্য করে?
উত্তর বুঝতে C-এর switch আসলে কী, সেটা দেখতে হবে। C-তে case 3: কোনো আলাদা block নয় — এটি শুধু একটি লেবেল, কোডের একটি জায়গার নাম, অনেকটা goto-র লেবেলের মতো। switch মানটি দেখে সেই লেবেলে লাফ দেয়, তারপর সাধারণ কোডের মতোই নিচে চলতে থাকে। break হলো “এখন switch-এর শেষে লাফ দাও।” C-এর এই নকশা এসেছে তার পূর্বসূরি ভাষা BCPL ও B-এর ধাঁচ থেকে, আর মেশিনের স্তরে এটি খুবই স্বাভাবিক: compiler প্রায়ই একটি “jump table” বানায় — মানটিকে সূচক ধরে সরাসরি সঠিক ঠিকানায় লাফ।
এই নকশার একটি বিখ্যাত চতুর ব্যবহার হলো “Duff's device” (১৯৮৩, Tom Duff): একটি do-while লুপের ভেতরে switch-এর case লেবেল ছড়িয়ে দিয়ে লুপ unroll করা। এটি বৈধ C, কারণ case শুধুই লেবেল। এটি দেখায় ভাষাটি কত নিম্নস্তরের চিন্তায় তৈরি।
কিন্তু বাস্তবে বেশিরভাগ case-এর শেষে break-ই লেখা হয়; গড়িয়ে পড়া দরকার হয় খুব কম ক্ষেত্রে। অর্থাৎ ডিফল্ট আচরণটি সাধারণ প্রয়োজনের উল্টো, আর break ভুলে যাওয়া দশকের পর দশক বাগের উৎস হয়ে আছে। পরের ভাষাগুলো তাই শিক্ষা নিয়েছে: C#-এ কাজসহ case থেকে গড়িয়ে পড়া compile error; Go আর Swift-এ ডিফল্টভাবে গড়ায় না, গড়াতে চাইলে স্পষ্ট fallthrough শব্দ লিখতে হয়।
C++ এই পথে যেতে পারেনি, কারণ কোটি কোটি লাইনের পুরোনো C ও C++ কোড ইচ্ছাকৃত fall-through-এর ওপর নির্ভর করে — নিয়ম বদলালে সেগুলো নিঃশব্দে ভিন্ন আচরণ করত। তাই C++17 একটি মাঝামাঝি সমাধান আনে: [[fallthrough]] attribute. এটি আচরণ বদলায় না, শুধু উদ্দেশ্য লিখে রাখে। তখন compiler সতর্কবার্তা (-Wimplicit-fallthrough) দিতে পারে শুধু সেই জায়গাগুলোয়, যেখানে গড়িয়ে পড়া হচ্ছে অথচ ঘোষণা নেই। পুরোনো কোড ভাঙে না, নতুন কোড নিরাপদ হয়। C23-ও একই attribute নিয়েছে।
মূল উপলব্ধি: পুরোনো ভাষায় ভুল ডিফল্ট বদলানো প্রায় অসম্ভব; তখন উপায় হলো উদ্দেশ্য স্পষ্ট লেখার ব্যবস্থা করা আর compiler-কে পাহারায় বসানো। -Wall -Wextra চালু রাখা আর [[fallthrough]] লেখা — দুটি মিলেই নিরাপত্তা।
প্র ০৩Range-based for নাকি শুধু “syntax sugar” — ভেতরে আসলে কী হয়? আর এর একটি লুকানো ফাঁদ C++23-এ কেন ঠিক করতে হলো?
“Syntax sugar” মানে এমন লেখা, যা নতুন কোনো ক্ষমতা দেয় না, শুধু পুরোনো কাজটি সহজে লিখতে দেয়। Range-for ঠিক তাই। C++11-এ আসা for (auto& x : v) { ... }-কে compiler মোটামুটি এভাবে পড়ে:
{ auto&& r = v; auto it = r.begin(); auto end = r.end(); for (; it != end; ++it) { auto& x = *it; ... } }
অর্থাৎ ভেতরে একটি সাধারণ iterator লুপ (পর্ব ৩২)। কোনো জিনিসকে range-for-এ চালাতে হলে তার শুধু begin() আর end() থাকলেই হয় — array-এর ক্ষেত্রে compiler নিজেই প্রথম ও শেষের পরের ঠিকানা নেয়। এজন্যই std::vector, std::string, std::map, এমনকি আপনার বানানো class-ও range-for-এ চলতে পারে। C++17 নিয়মটি একটু শিথিল করে, যাতে begin আর end-এর টাইপ আলাদা হতে পারে — C++20-এর ranges-এর জন্য এটি জরুরি ছিল।
তাহলে এত সহজ জিনিসের মূল্য কী? ভুলের জায়গা কমানো। সূচক লুপে আপনি শুরু, শেষ আর বৃদ্ধি তিনটিই নিজে লেখেন — তিনটিই off-by-one-এর সুযোগ। Range-for-এ এর কোনোটিই আপনার হাতে নেই। একই কারণে অন্য ভাষাতেও এটি আছে: Java-র “for-each”, Python-এর for x in list.
এবার লুকানো ফাঁদ। ভেতরের প্রথম লাইন auto&& r = ... — এটি range-এর মানটিকে একটি reference-এ ধরে রাখে। for (auto x : get_list())-এ ফেরত আসা অস্থায়ী (temporary) মানের আয়ু reference-এর কারণে লুপ জুড়ে বাড়ানো হয়, তাই এটি নিরাপদ। কিন্তু for (auto x : get_student().marks) বা অস্থায়ী মানের ভেতরের কোনো অংশের ক্ষেত্রে, C++20 পর্যন্ত মাঝের অস্থায়ী object লুপ শুরুর আগেই ধ্বংস হয়ে যেতে পারত — লুপটি ঘুরত মরে যাওয়া মেমরির ওপর, যা dangling reference (পর্ব ১৮)। বহু বছর ধরে এটি অভিজ্ঞদেরও বিপদে ফেলেছে। C++23-এ একটি প্রস্তাব গৃহীত হয়, যাতে range-for-এর range অংশে তৈরি সব অস্থায়ী object লুপ শেষ হওয়া পর্যন্ত বাঁচে।
মূল উপলব্ধি: syntax sugar-ও ভেতরের নিয়ম মেনেই চলে। Range-for বুঝতে হলে iterator আর object-এর আয়ু বুঝতে হয় — আর সেটা বুঝলেই বুঝবেন কেন লুপের ভেতরে container-এর আকার বদলানো বিপজ্জনক।
প্র ০৪যেকোনো লুপকে recursion দিয়ে, আর যেকোনো recursion-কে লুপ দিয়ে লেখা যায়। তাহলে কখন recursion লুপের চেয়ে ভালো, আর C++-এ কখন বিপজ্জনক?
Recursion মানে একটি ফাংশন নিজেকেই ডাকে, প্রতিবার সমস্যার একটি ছোট রূপ দিয়ে (ফাংশন পর্ব ০৯-এ; গভীর আলোচনা C কোর্সের recursion পর্বে)। তাত্ত্বিকভাবে দুটি সমান শক্তিশালী — একটি দিয়ে যা লেখা যায়, অন্যটি দিয়েও যায়। প্রশ্নটা তাই শক্তির নয়, স্পষ্টতা আর খরচের।
Recursion যেখানে জেতে: যখন সমস্যার আকারটাই নিজের ছোট রূপ দিয়ে তৈরি। একটি ফোল্ডারের ভেতরে ফোল্ডার, তার ভেতরে আরও — মোট ফাইলের আকার বের করা। একটি পরিবারের বংশতালিকা, বা কোনো কোম্পানির পদের ক্রম (tree). Merge sort বা quick sort-এর মতো “ভাগ করো, জয় করো” algorithm. এসব লুপ দিয়ে লিখতে হলে নিজেকেই একটি stack বানিয়ে কোন ফোল্ডার বাকি আছে তার হিসাব রাখতে হয় — কোড লম্বা আর ভুলপ্রবণ হয়। Recursion-এ সেই হিসাব রাখে ভাষা নিজেই, call stack দিয়ে।
লুপ যেখানে জেতে: সোজা, একমাত্রিক কাজে — ১ থেকে n যোগ, তালিকার সর্বোচ্চ, ফাইলের প্রতিটি লাইন পড়া। এখানে recursion শুধু জটিলতা বাড়ায়।
C++-এ বিপদ: প্রতিটি ফাংশন কল call stack-এ কিছু জায়গা নেয়, আর stack-এর আকার সীমিত — অনেক সিস্টেমে মূল thread-এর জন্য কয়েক মেগাবাইট। দশ লাখ উপাদানের তালিকা recursion দিয়ে হাঁটলে stack overflow হয়ে প্রোগ্রাম ভেঙে পড়তে পারে। কিছু ভাষা (যেমন Scheme) “tail call”-কে লুপে বদলে দেওয়ার নিশ্চয়তা দেয়; C++ standard তা দেয় না। GCC বা Clang optimization চালু থাকলে প্রায়ই করে, কিন্তু debug build-এ নাও করতে পারে — একই কোড এক জায়গায় চলে, আরেক জায়গায় ভাঙে।
আরেকটি খরচ পুনরাবৃত্ত কাজ। Fibonacci-এর সরল recursive সংজ্ঞা একই মান বারবার হিসাব করে, সময় বাড়ে সূচকীয় হারে। লুপে বা memoization দিয়ে একই কাজ রৈখিক সময়ে হয়। কনটেস্ট প্রোগ্রামিংয়ে (পর্ব ৫০) এই পার্থক্যই Time Limit Exceeded আর Accepted-এর মধ্যে।
মূল উপলব্ধি: ডেটার আকার যদি গাছের মতো শাখা-প্রশাখায় ছড়ানো হয় এবং গভীরতা সীমিত, recursion কোডকে সমস্যার মতো দেখায়। ডেটা যদি সোজা লম্বা সারি হয় বা গভীরতা অনেক বড় হতে পারে, লুপ নিরাপদ ও দ্রুত।
প্র ০৫Compiler কি কোনো লুপ দেখে নিশ্চিত বলতে পারে যে সেটা একদিন থামবে? না পারলে, C++ compiler অসীম লুপ নিয়ে কী ধরে নেয়?
১৯৩৬ সালে Alan Turing একটি গভীর ফল প্রমাণ করেন, যার নাম এখন halting problem: এমন কোনো সাধারণ প্রোগ্রাম লেখা অসম্ভব, যা যেকোনো প্রোগ্রাম আর তার ইনপুট দেখে সবসময় সঠিকভাবে বলে দেবে “এটি থামবে” বা “এটি চিরকাল চলবে।”
প্রমাণের মূল চিন্তা একটি চতুর উল্টো যুক্তি। ধরুন এমন একটি নিখুঁত বিচারক halts(P) আছে। এখন একটি দুষ্টু প্রোগ্রাম বানাই: “বিচারক যদি বলে আমি থামব, তাহলে আমি চিরকাল লুপে ঘুরব; যদি বলে থামব না, তাহলে সাথে সাথে থেমে যাব।” এবার এই প্রোগ্রামটিকে নিজের সম্পর্কেই বিচারকের কাছে পাঠাই। বিচারক যে উত্তরই দিক, দুষ্টু প্রোগ্রাম তার উল্টো করবে — অর্থাৎ বিচারক ভুল। তাই নিখুঁত বিচারক থাকতেই পারে না। এটা প্রযুক্তির সীমা নয়, গণিতের সীমা — যত শক্তিশালী কম্পিউটারই আসুক।
এর মানে এই নয় যে compiler কিছুই বলতে পারে না। for (int i = 0; i < 10; ++i) যে থামবে, তা সহজেই বোঝা যায়। কিন্তু Collatz-এর বিখ্যাত প্রশ্ন ভাবুন — “n জোড় হলে অর্ধেক করো, বিজোড় হলে 3n + 1; একদিন কি 1-এ পৌঁছাবেই?” চার লাইনের লুপ, অথচ গণিতবিদেরা আজও প্রমাণ করতে পারেননি যে এটি সব n-এর জন্য থামে। Compiler তো দূরের কথা।
তাহলে C++ compiler কী করে? Standard-এ একটি “forward progress” নিয়ম আছে: যে লুপ বাইরের জগতের সাথে কিছুই করে না — কোনো ইনপুট-আউটপুট নেই, volatile বা atomic কাজ নেই, thread-এর সমন্বয় নেই — compiler ধরে নিতে পারে সেটি একদিন থামবে। এমন লুপ সত্যিই চিরকাল চললে তা undefined behavior. এতে optimizer লুপের হিসাব সরাতে বা সাজাতে পারে, “এটা থামে কি না” প্রমাণ না করেই। ফল কখনো অবাক করা — Clang-এর মতো compiler এমন খালি অসীম লুপ পুরোপুরি মুছে ফেলার উদাহরণ আছে। এই অদ্ভুততা কমাতে C++26-এর জন্য গৃহীত একটি প্রস্তাবে while (true) {}-এর মতো শর্ত ধ্রুবক এমন সরল অসীম লুপকে UB-এর বাইরে আনা হয়েছে, কারণ embedded সিস্টেমে এগুলো ইচ্ছা করেই লেখা হয়।
মূল উপলব্ধি: লুপ থামবে কি না, তার নিশ্চয়তা কোনো টুল সাধারণভাবে দিতে পারে না — দায়িত্ব প্রোগ্রামারের। তাই প্রতিটি লুপে একটি “এগিয়ে যাওয়ার মাপ” রাখুন, যা প্রতি iteration-এ শেষের দিকে এগোয়, আর ইচ্ছাকৃত অসীম লুপে বাইরের কোনো কাজ (I/O, অপেক্ষা) রাখুন।
অনুশীলন
বেশিরভাগ কাজে কোড লিখতে হবে না — আগে অনুমান করুন, তারপর উত্তর মিলিয়ে নিন।
-
বিল হাতে হিসাব: অংশ ২-এর কাল্পনিক হারে ৭৫, ২০০ আর ৩০১ ইউনিটের বিল কত? আর কেউ যদি শর্তগুলো উল্টো ক্রমে লেখেন (আগে
units <= 300, তারপর<= 200, তারপর<= 75), তাহলে ৫০ ইউনিটের বিল কত আসবে?৭৫ ইউনিট: প্রথম শর্ত সত্য, ৭৫ × ৫ = ৩৭৫. ২০০ ইউনিট: দ্বিতীয় শর্ত, ৩৭৫ + ১২৫ × ৭ = ১২৫০. ৩০১ ইউনিট:
else, ২০৫০ + ১ × ১০ = ২০৬০. উল্টো ক্রমে ৫০ ইউনিট প্রথমেইunits <= 300-এ সত্য হয়ে যায়, তাই সূত্র1250 + (50 - 200) * 8= ১২৫০ − ১২০০ = ৫০ টাকা — সম্পূর্ণ ভুল। শিক্ষা:else ifসিঁড়িতে প্রথম সত্য শর্ত জেতে, তাই ক্রমই যুক্তির অংশ। -
Trace table বানান:
int x = 20, steps{}; while (x != 1) { if (x % 2 == 0) x /= 2; else x = 3 * x + 1; ++steps; }— প্রতিটি iteration-এxআরstepsলিখুন। লুপ শেষেstepsকত?x-এর ক্রম: 20 → 10 → 5 → 16 → 8 → 4 → 2 → 1. প্রতিটি তীর একটি iteration, তাইsteps= ৭. এটিই Collatz-এর প্রশ্ন (ভাবনার প্রশ্ন ০৫) — ২০-এর জন্য থামে, কিন্তু সব সংখ্যার জন্য থামবে কি না, তা আজও অপ্রমাণিত। লক্ষ করুন, এখানে “শেষের দিকে এগিয়ে যাওয়ার মাপ” স্পষ্ট নয় —xকখনো বাড়ে, কখনো কমে। -
বাগ শিকার: একটি AI লিখেছে —
std::vector<std::string> names{...}; for (int i = 0; i <= names.size(); i++) { auto n = names[i]; n += " Khan"; }. উদ্দেশ্য ছিল প্রতিটি নামের শেষে “Khan” যোগ করা। কয়টি সমস্যা খুঁজে পান?চারটি: (১)
<=— off-by-one, শেষেnames[names.size()]সীমার বাইরে, undefined behavior. (২)intবনামsize()— signed-unsigned তুলনার সতর্কবার্তা। (৩)auto nকপি — যোগ হয় কপিতে,namesএকটুও বদলায় না। (৪) প্রতিবার পুরো string-এর অকারণ কপি। সঠিক রূপ এক লাইনে:for (auto& n : names) n += " Khan"; -
নিজে চালিয়ে দেখুন (কোড): উপরের
marks.cpp-এcase 5: grade = 'D'; break;যোগ করুন, আর grace-এর লুপেauto&-কেautoকরে দিন। আউটপুট কী বদলাবে, আগে অনুমান করে তারপর চালান। কোনো warning এলো কি?autoকরলে grace আর আসল vector-এ পৌঁছায় না, তাই নম্বর থাকে 72, 45, 88, 91, 67 আর আউটপুট দাঁড়ায়72:B 45:F 88:A 91:A 67:C— 45 এখনো F, কারণ 4 কোনো case-এ নেই। Compiler-এর সংস্করণভেদে “set but not used”-ধাঁচের সতর্কবার্তা আসতে পারে, নাও আসতে পারে — অর্থাৎ এই বাগের জন্য compiler-এর ওপর ভরসা করা যায় না।auto&রেখেcase 5যোগ করলে 50 পায় D:77:B 50:D 93:A 96:A 72:B.
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমfor (init; cond; step) { body } লুপের প্রথম দুই iteration-এ কাজগুলো যে ক্রমে ঘটে, সেভাবে সাজান (▲▼ চেপে সরান)
init— একবারই চলেcondপরীক্ষা — সত্যbody— প্রথম iterationstep— যেমন++icondআবার পরীক্ষাbody— দ্বিতীয় iteration
C++-এ int sum{}; for (int i = 1; i <= 4; ++i) sum += i; চলার পর sum-এর মান কত, আর শর্ত i <= 4 মোট কতবার পরীক্ষা হয়?
- sum = 10, শর্ত ৪ বার
- sum = 15, শর্ত ৫ বার
- sum = 10, শর্ত ৫ বার
- sum = 6, শর্ত ৪ বার
C++-এ std::vector<int> v{1, 2, 3}; for (auto x : v) x *= 10; চলার পর v-তে কী থাকে?
- {10, 20, 30}
- {1, 2, 3}
- compile error
- {1, 2, 30}
C++-এ int k = 1; switch (k) { case 1: std::print("A"); case 2: std::print("B"); break; case 3: std::print("C"); } কী ছাপায়?
- A
- B
- ABC
- AB
C++17-এর if (auto pos = s.find('@'); pos != std::string::npos) { ... } লেখার প্রধান সুবিধা কী?
posশুধুif/else-এর ভেতরে থাকে, বাইরে ব্যবহার করা যায় না- খোঁজা দ্বিগুণ দ্রুত হয়
- শর্তটি compile-এর সময় পরীক্ষা হয়
- শর্ত লেখার দরকার হয় না
C++-এ কোন লুপটি তার শর্ত মিথ্যা হলেও দেহের কাজ অন্তত একবার করে?
- while
- do-while
- for
- range-based for
std::size_t n = 5; হলে C++-এ for (std::size_t i = n - 1; i >= 0; --i) লুপটি কী করে?
- ৪ থেকে ০ পর্যন্ত ৫ বার চলে থামে
- compile error দেয়
- কখনো থামে না — অসীম লুপ
- একবারও চলে না
C++-এ switch সম্পর্কে কোন কথাটি সঠিক?
std::stringদিয়েswitchকরা যায়- case-এ যেকোনো ভ্যারিয়েবল লেখা যায়
defaultনা দিলে compile error হয়- C++17-এর
[[fallthrough]]ইচ্ছাকৃত fall-through চিহ্নিত করে, ফলে সেখানে সতর্কবার্তা আসে না
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
else if-এর সিঁড়িতে প্রথম সত্য শর্তটিই জেতে — শর্তের ক্রম যুক্তির অংশ। পিরামিডের বদলে guard clause দিয়ে ব্যতিক্রম আগে সরান।- C++17-এর
if (init; cond)ভ্যারিয়েবলের scope ছোট রাখে; scope যত ছোট, ভুল তত কম। switch-এ কাজসহ প্রতিটি case-এর শেষেbreak, ইচ্ছাকৃত গড়িয়ে পড়ায়[[fallthrough]], আর সংখ্যায় সবসময়default.- container-এ
for (const auto& x : v); বদলাতেauto&. শুধুautoমানে কপি — আসলটি বদলায় না। - প্রতিটি লুপে জানুন কতবার চলবে আর কেন থামবে: half-open
[0, n),.size()-এর সাথেintনয়, আর nested লুপে খরচ n².
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ০৯ · ফাংশন ও overloadingপরবর্তী পর্বকোডকে নামযুক্ত, পুনর্ব্যবহারযোগ্য টুকরোয় ভাগ করা।
- C++ প্রোগ্রামিং · কোর্স হোম৫৫টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- C প্রোগ্রামিং · লুপগভীরে যানwhile, do-while, for-এর আরও উদাহরণ আর খুঁটিনাটি।
- C প্রোগ্রামিং · Big-O ও সময়ের জটিলতাপাশের ট্র্যাকnested লুপের খরচ কীভাবে মাপবেন।