এই পর্বে যা শিখবেন
- গণিতের অপারেটর — integer division, ভাগশেষ আর ঋণাত্মক সংখ্যায় তাদের আচরণ
- assignment,
+=-এর মতো সংক্ষিপ্ত রূপ, আর++iবনামi++-এর আসল পার্থক্য - তুলনা ও যুক্তির অপারেটর, short-circuit দিয়ে নিরাপদ শর্ত লেখা, আর
bool-intসম্পর্ক - bitwise অপারেটর দিয়ে অনুমতির (permission) হিসাব, precedence ও associativity
- ternary
?:, comma অপারেটর, আর AI-এর লেখা কোডে অপারেটরের ভুল ধরার উপায়
১ · মডিউল ২ শুরু — বাক্স থেকে হিসাবে
প্রথম মডিউলে আপনি শিখেছেন C++ কেন, প্রোগ্রাম কীভাবে compile হয়, আর ডেটা কোন টাইপের বাক্সে থাকে। এবার শুরু হলো মডিউল ২ — “ভাষার মূল যন্ত্রপাতি”. সাতটি পর্বে আপনি সেই যন্ত্রগুলো হাতে নেবেন, যা প্রায় প্রতিটি লাইনে লাগে: অপারেটর (এই পর্ব), ইনপুট-আউটপুট (পর্ব ০৭), সিদ্ধান্ত ও লুপ (পর্ব ০৮), ফাংশন (পর্ব ০৯), reference (পর্ব ১০), namespace ও header (পর্ব ১১), আর enum ও struct (পর্ব ১২)। মডিউল শেষে আপনি ছোট কিন্তু সত্যিকারের কাজের প্রোগ্রাম লিখতে পারবেন।
আগের পর্বে বাক্স বানিয়েছেন। কিন্তু বাক্সে মান রেখে বসে থাকলে কোনো কাজ হয় না। দোকানের বিল যোগ করতে হয়, নম্বর দেখে গ্রেড ঠিক করতে হয়, বছরটা অধিবর্ষ কি না যাচাই করতে হয়। এসব কাজের যন্ত্রই হলো operator — এমন চিহ্ন, যা এক বা একাধিক মান নিয়ে একটি নতুন মান তৈরি করে।
ক্যালকুলেটরের কথা ভাবুন। সংখ্যার বোতামগুলো হলো মান, আর +, −, ×, ÷ বোতামগুলো অপারেটর। অপারেটর যে মানগুলোর ওপর কাজ করে, তাদের বলে operand. মান আর অপারেটর মিলে যে হিসাবের টুকরোটা তৈরি হয় — যেমন price * 3 — তার নাম expression. প্রতিটি expression শেষে একটি মান দেয়, আর সেই মানের একটি টাইপ থাকে।
Operand কয়টি, সেই অনুযায়ী অপারেটর তিন রকম:
১ · Unary
একটি operand: -x, !done, ++count. চিহ্নটি মানের আগে বা পরে বসে।
২ · Binary
দুটি operand: a + b, marks >= 80, x && y. বেশিরভাগ অপারেটর এই দলের।
৩ · Ternary
তিনটি operand — C++-এ এমন অপারেটর একটিই: cond ? a : b. অংশ ৮-এ দেখবেন।
প্রতিটি অপারেটরের ফল নির্ভর করে operand-এর টাইপের ওপর। একই / চিহ্ন int-এ দশমিক কেটে ফেলে, double-এ রাখে। তাই অপারেটর পড়ার সময় সবসময় দুটি প্রশ্ন করুন: “দুই পাশের টাইপ কী?” আর “ফলের টাইপ কী?” আগের পর্বের টাইপের জ্ঞান এখানেই প্রতিদিন কাজে লাগবে।
২ · গণিতের অপারেটর — যোগ, বিয়োগ, ভাগ আর ভাগশেষ
পাঁচটি arithmetic operator আছে: + (যোগ), - (বিয়োগ), * (গুণ), / (ভাগ) আর % (ভাগশেষ)। প্রথম তিনটি স্কুলের অঙ্কের মতোই কাজ করে — শুধু মনে রাখবেন, int-এর সীমা পার হলে overflow হয় (পর্ব ০৫)। আসল মজা আর আসল ফাঁদ শেষের দুটিতে।
ভাগ: int হলে দশমিক কাটা যায়
আগের পর্বে দেখেছেন, দুটি int ভাগ করলে ফলও int — একে বলে integer division. 17 / 5 = 3, 2 / 3 = 0. Rounding হয় না, সোজা কেটে ফেলা হয়। অন্তত একটি operand double হলে ভাগ দশমিকে হয়: 17.0 / 5 = 3.4.
কিন্তু integer division সবসময় শত্রু নয়, অনেক সময় এটাই আপনার দরকার। ধরুন একজন দোকানদার ১২৪০ টাকা ভাংতি দেবেন একশো টাকার নোটে। কয়টি নোট? 1240 / 100 = 12. বাকি কত থাকল? সেটা দেয় ভাগশেষের অপারেটর।
ভাগশেষ: %
% অপারেটর দুটি পূর্ণসংখ্যা ভাগ করে ভাগশেষ দেয়: 1240 % 100 = 40. এর ব্যবহার ভাবনার চেয়ে অনেক বেশি:
| কাজ | লেখা | ব্যাখ্যা |
|---|---|---|
| জোড় না বিজোড় | n % 2 == 0 | ২ দিয়ে ভাগ করে কিছু না থাকলে জোড় |
| শেষ অঙ্ক | n % 10 | 2026 % 10 = 6; বাকি অঙ্ক পেতে n / 10 |
| ঘড়ির হিসাব | (hour + 5) % 24 | রাত ২২টার ৫ ঘণ্টা পরে ৩টা, ২৭টা নয় |
| পালা করে কাজ | day % 7 | সপ্তাহের কোন দিন, বা ৩ জন রিকশাচালকের মধ্যে আজ কার পালা (day % 3) |
| মিনিট থেকে ঘণ্টা-মিনিট | 150 / 60, 150 % 60 | ২ ঘণ্টা ৩০ মিনিট |
দুটি জিনিস মনে রাখুন। প্রথমত, % শুধু পূর্ণসংখ্যায় চলে — 7.5 % 2 লিখলে compile error; দশমিকের ভাগশেষ লাগলে <cmath>-এর std::fmod. দ্বিতীয়ত, শূন্য দিয়ে পূর্ণসংখ্যার ভাগ বা ভাগশেষ (x / 0, x % 0) undefined behavior — বেশিরভাগ কম্পিউটারে প্রোগ্রাম সাথে সাথে ভেঙে পড়ে। দশমিকে শূন্য দিয়ে ভাগ করলে IEEE 754 মেনে চলা সিস্টেমে বাস্তবে inf আসে, তবে C++ standard এটাকেও নিশ্চিত আচরণ বলে না। নিয়ম একটাই: ভাগের আগে ভাজক শূন্য কি না দেখে নিন — কীভাবে নিরাপদে দেখবেন, অংশ ৫-এ।
ঋণাত্মক সংখ্যার ভাগ — শূন্যের দিকে কাটা
-7 / 2 কত? গণিতে −৩.৫. পূর্ণসংখ্যায় দুটি উত্তর সম্ভব: −৩ (শূন্যের দিকে কাটা) বা −৪ (ছোটর দিকে নামানো, যাকে বলে floor). C++11 থেকে standard স্পষ্ট বলে: ভাগফল শূন্যের দিকে কাটা হবে, তাই -7 / 2 = -3. আর ভাগশেষ এমনভাবে ঠিক হয় যেন সবসময় এই সম্পর্কটি সত্য থাকে:
(a / b) * b + a % b == aতাহলে -7 % 2 = −৭ − (−৩ × ২) = -1. ফলাফলে একটি সহজ নিয়ম বেরিয়ে আসে: ভাগশেষের চিহ্ন বাম পাশের সংখ্যার (ভাজ্যের) চিহ্নের মতো। 7 % -2 = 1, -7 % -2 = -1.
এই নিয়মের কারণে একটি খুব প্রচলিত বাগ হয়: বিজোড় যাচাইয়ে n % 2 == 1 লেখা। n ঋণাত্মক বিজোড় হলে (যেমন −৭) ভাগশেষ -1, তাই শর্ত মিথ্যা — −৭ “বিজোড় নয়” ধরা হয়! সঠিক লেখা n % 2 != 0. আর ঘড়ির মতো সবসময় ধনাত্মক ভাগশেষ চাইলে লিখুন ((a % m) + m) % m. Python-এ -7 % 2 হয় 1, কারণ Python floor নিয়ম মানে — তাই Python থেকে কোড অনুবাদ করার সময় (মানুষ বা AI যে-ই করুক) এখানে ভুল ঢোকে।
বাস্তব উদাহরণ: দোকানের বিল
একটি মুদি দোকানের বিল ভাবুন — ৩ কেজি চাল, ২ লিটার তেল, ১২টি ডিম। মোট ১০০০ টাকা বা বেশি হলে ১০% ছাড়, তারপর ছাড়ের পরের দামে ১৫% VAT. সব হিসাব পূর্ণসংখ্যায় রাখা হয়েছে, যাতে দশমিকের “প্রায়-সঠিক” ঝামেলা না থাকে:
#include <print>
int main() {
constexpr int VAT_PERCENT = 15;
int subtotal{}; // taka
subtotal += 3 * 120; // 3 kg chal
subtotal += 2 * 350; // 2 litre tel
subtotal += 12 * 15; // 12 ta dim
int discount = (subtotal >= 1000) ? subtotal / 10 : 0; // 10% chhar
int afterDiscount = subtotal - discount;
int vat = afterDiscount * VAT_PERCENT / 100;
std::println("subtotal = {}, discount = {}", subtotal, discount);
std::println("VAT = {}, total = {}", vat, afterDiscount + vat);
std::println("1240 taka: {} notes of 100, {} left", 1240 / 100, 1240 % 100);
std::println("-7 / 2 = {}, -7 % 2 = {}", -7 / 2, -7 % 2);
}মোট দাম আসে ১২৪০ (৩৬০ + ৭০০ + ১৮০), তাই ছাড় ১২৪ টাকা, আর ছাড়ের পরে ১১১৬. VAT-এর লাইনটি খেয়াল করুন: afterDiscount * VAT_PERCENT / 100. আগে গুণ, পরে ভাগ — ১১১৬ × ১৫ = ১৬৭৪০, ১০০ দিয়ে ভাগ করে 167 (আসল ১৬৭.৪, দশমিক কাটা গেল)। যদি VAT_PERCENT / 100 * afterDiscount লিখতেন, তাহলে প্রথমেই 15 / 100 = 0, আর VAT হতো শূন্য! মোট আসে ১২৮৩. শেষ লাইন দেখায় -7 / 2 = -3 আর -7 % 2 = -1. মাঝের ? : চিহ্নটি ternary অপারেটর — অংশ ৮-এ বিস্তারিত।
দশমিক কাটা না রাউন্ড? বাস্তব VAT-এর হিসাবে কীভাবে রাউন্ড করতে হবে, তা আইন বা প্রতিষ্ঠানের নিয়ম ঠিক করে — প্রোগ্রামার নয়। কোড লেখার আগে নিয়মটি জেনে নিন, আর কোডে স্পষ্ট করে লিখুন। পূর্ণসংখ্যায় “কাছের সংখ্যায় রাউন্ড” করার একটি পুরোনো কৌশল: (x * 15 + 50) / 100 (ধনাত্মক x-এর জন্য)।
৩ · Assignment, += আর ++ — মান বদলানোর অপারেটর
= চিহ্নটি C++-এ “সমান” নয়, এর অর্থ “ডান পাশের মান বাম পাশের বাক্সে রাখো”। তাই count = count + 1; গণিতে অসম্ভব হলেও প্রোগ্রামে খুবই স্বাভাবিক: পুরোনো মানে ১ যোগ করে আবার একই বাক্সে রাখা। সমান কি না যাচাই করার অপারেটর আলাদা — == (অংশ ৪)।
একটি অদ্ভুত কিন্তু জরুরি তথ্য: assignment নিজেও একটি expression, এর একটি মান আছে — বাম পাশের ভ্যারিয়েবলটি। তাই a = b = 0; চলে: আগে b = 0, তারপর সেই ফল a-তে। অর্থাৎ = ডান থেকে বামে কাজ করে। এই সুবিধার একটি অন্ধকার দিকও আছে, যা অংশ ৪-এ দেখবেন।
সংক্ষিপ্ত রূপ — compound assignment
“নিজের সাথে কিছু করে আবার নিজেতেই রাখা” এত বেশি লাগে যে এর জন্য আলাদা লেখা আছে:
| সংক্ষিপ্ত | মানে | উদাহরণ |
|---|---|---|
x += 5 | x = x + 5 | বিলে নতুন পণ্যের দাম যোগ |
x -= 5 | x = x - 5 | bKash ব্যালান্স থেকে খরচ বাদ |
x *= 2 | x = x * 2 | দ্বিগুণ করা |
x /= 10 | x = x / 10 | শেষ অঙ্ক ফেলে দেওয়া |
x %= 7 | x = x % 7 | সপ্তাহের চক্রে ফেরা |
সংক্ষিপ্ত রূপ শুধু টাইপ করা কমায় না। scores[findIndex(roll)] += 5; লিখলে বাম পাশের অংশটি একবারই হিসাব হয়; লম্বা রূপে findIndex দুবার চলত। তবে একটি লুকোনো ফাঁদ আছে: int taka = 10; taka += 2.7; কোনো error ছাড়াই compile হয়, আর taka হয় 12 — ১২.৭-এর দশমিক নিঃশব্দে কেটে গেল। Compound assignment ফলকে বাম পাশের টাইপে বদলে নেয়, আর এখানে আগের পর্বের {} পাহারাদার নেই। GCC-তে -Wconversion দিলে সতর্কবার্তা পাবেন।
++ আর -- — এক বাড়ানো, এক কমানো
১ যোগ করা সবচেয়ে বেশি লাগে (গোনা, লুপ চালানো), তাই এর জন্য আছে increment ও decrement অপারেটর: ++ আর --. এরা দুই জায়গায় বসতে পারে, আর জায়গা অনুযায়ী ফল আলাদা:
- Prefix
++i: আগে বাড়াও, তারপর নতুন মানটি দাও। - Postfix
i++: পুরোনো মানটি দাও, আর ভ্যারিয়েবলটিকে বাড়িয়ে রাখো।
লাইন একা থাকলে (i++; বা ++i;) দুটির ফল একই — i এক বাড়ে। পার্থক্য দেখা যায় যখন মানটি অন্য কোথাও ব্যবহার হয়। ধাপে ধাপে দেখুন:
1int i = 5;2int a = i++;3int b = ++i;4int n = 0;5while (n < 3) std::print("{} ", n++);6int m = 0;7while (m < 3) std::print("{} ", ++m);8for (int k = 0; k < 3; ++k) { }
| i | |
| a | |
| b | |
| n | |
| m |
- শুরু।
i-এ 5. এবার একই ভ্যারিয়েবলে দুই রকম increment চালানো হবে। - Postfix: আগে দাও, পরে বাড়াও।
a = i++—i-এর পুরোনো মান 5a-তে গেল, আরiহলো 6. যেন চায়ের দোকানে আগের কাপের দাম লিখে তারপর গোনার খাতায় এক যোগ করা। - Prefix: আগে বাড়াও, পরে দাও।
b = ++i—iআগে 7 হলো, তারপর সেই নতুন 7b-তে গেল। তাইa= 5 কিন্তুb= 7. - লুপে
n++. প্রতিবার পুরোনো মান ছাপা হয়, তারপর বাড়ে: ছাপা হয়0 1 2. যখনn3 হয়, শর্তn < 3মিথ্যা — লুপ থামে। - লুপে
++m. এবার আগে বাড়ে, তারপর ছাপা হয়:1 2 3. একই লুপ, একই শর্ত — শুধু++-এর জায়গা বদলে আউটপুট এক ঘর সরে গেল। এমন “এক ঘর সরে যাওয়া” বাগকে বলে off-by-one. forলুপে — পার্থক্য নেই।for-এর তৃতীয় অংশে++kবাk++লিখলে একই ফল, কারণ সেখানে মানটি কেউ ব্যবহার করে না। C++-এ অনেকে অভ্যাসবশত++kলেখেন:int-এ কোনো পার্থক্য নেই, কিন্তু পরে দেখবেন এমন iterator-এ (পর্ব ৩২) postfix-কে পুরোনো মানের একটি কপি বানাতে হতে পারে। লুপ নিয়ে পুরো আলোচনা পর্ব ০৮-এ।
একই expression-এ মিশাবেন না
++ একটি মান দেয় এবং ভ্যারিয়েবল বদলায়। এই দ্বিতীয় কাজটিকে বলে side effect. একটি expression-এর ভেতরে একই ভ্যারিয়েবলে দুটি side effect, বা একটি side effect আর সেই ভ্যারিয়েবলের অন্য ব্যবহার থাকলে, প্রশ্ন ওঠে — কোনটা আগে? C++ সব ক্ষেত্রে এর উত্তর দেয় না। কোন হিসাব কোনটির আগে হতেই হবে, সেই নিয়মের নাম sequencing.
| লেখা | C++17 থেকে অবস্থা |
|---|---|
i++ + i++ | undefined behavior — দুটি বদলের মধ্যে কোনো ক্রম নেই |
i = ++i + i++ | undefined behavior |
i = i++ | C++17 থেকে সংজ্ঞায়িত (i আগের মানেই থাকে), C++17-এর আগে undefined — কিন্তু অর্থহীন লেখা |
f(i++, i++) | C++17 থেকে undefined নয়, কিন্তু কোন argument আগে, তা compiler ঠিক করে (unspecified) |
std::cout << i << i++ | C++17 থেকে বাম থেকে ডানে নিশ্চিত; আগে undefined |
নিয়ম মুখস্থ করার দরকার নেই, অভ্যাস একটাই রাখুন: এক লাইনে একটি ভ্যারিয়েবল একবারই বদলান, আর বদলানো ভ্যারিয়েবল সেই লাইনে আর পড়বেন না। arr[i] = i++;-এর মতো লেখা ভেঙে দুই লাইনে লিখুন। কোন লেখা undefined আর কেন undefined behavior এত বিপজ্জনক — পুরো আলোচনা পর্ব ৪৩ · Undefined Behavior ও নিরাপদ C++-এ।
৪ · তুলনা — সমান, ছোট, বড়
ছয়টি comparison operator দুটি মান মিলিয়ে দেখে, আর ফল দেয় একটি bool — true বা false:
| অপারেটর | অর্থ | উদাহরণ (marks = 75) |
|---|---|---|
== | সমান | marks == 75 → true |
!= | সমান নয় | marks != 75 → false |
< | ছোট | marks < 33 → false (ফেল নয়) |
> | বড় | marks > 79 → false |
<= | ছোট বা সমান | marks <= 100 → true |
>= | বড় বা সমান | marks >= 70 → true (A গ্রেড) |
দেখতে সহজ, কিন্তু এখানে C++-এর সবচেয়ে বিখ্যাত তিনটি ফাঁদ লুকিয়ে আছে।
ফাঁদ ১ — = বনাম ==
if (balance = 0) — প্রোগ্রামার লিখতে চেয়েছিলেন “ব্যালান্স কি শূন্য?” কিন্তু একটি = কম পড়ায় এটা হয়ে গেল assignment: balance-এ 0 বসানো হলো, আর assignment-এর মান 0, যা শর্ত হিসেবে false. ফল দ্বিগুণ খারাপ — শর্ত কখনো সত্য হয় না, আর গ্রাহকের ব্যালান্স মুছে গেল! কোডটি বৈধ C++, তাই compile হয়। ভরসা compiler-এর সতর্কবার্তা: GCC-তে -Wall দিলে বলে suggest parentheses around assignment used as truth value. এজন্যই প্রতিটি প্রজেক্টে -Wall -Wextra চালু রাখুন (পর্ব ০৩)।
ফাঁদ ২ — 0 < x < 10 গণিতের মতো কাজ করে না
গণিতে লেখা যায় ০ < x < ১০. C++-এ 0 < x < 10 পড়া হয় (0 < x) < 10 — প্রথম তুলনার ফল true বা false, অর্থাৎ 1 বা 0, আর সেটা সবসময় 10-এর চেয়ে ছোট। ফলে শর্তটি সবসময় সত্য, x যা-ই হোক! সঠিক লেখা: 0 < x && x < 10. সুখবর: GCC আর Clang -Wall-এ এখানে সতর্ক করে।
ফাঁদ ৩ — signed আর unsigned মেশানো
পর্ব ০৫-এ unsigned টাইপ দেখেছেন। একটি signed আর একটি unsigned মান তুলনা করলে C++ signed মানটিকে unsigned-এ বদলে নেয়। −১-কে unsigned-এ বদলালে হয় সবচেয়ে বড় unsigned সংখ্যা (৩২ বিটে 4294967295). তাই -1 < 3u হয় false! বাস্তবে এটা সবচেয়ে বেশি ঘটে container-এর আকারের সাথে: v.size() unsigned, তাই int i = -1; i < v.size() মিথ্যা। এই signed-unsigned মেশানোয় GCC -Wall-এ -Wsign-compare সতর্কবার্তা দেয়। সমাধান তিনটি: দুই পাশ একই রকম টাইপে রাখা, C++20-এর std::ssize(v) দিয়ে signed আকার নেওয়া, বা C++20-এর <utility>-এর std::cmp_less(a, b) — যা গাণিতিকভাবে সঠিক তুলনা করে।
দশমিকে == নয়: আগের পর্বে দেখেছেন 0.1 + 0.2 == 0.3 মিথ্যা। দুটি double “প্রায় সমান” কি না দেখতে পার্থক্যের পরম মান একটি ছোট সীমার সাথে মেলান: std::abs(a - b) < 1e-9. কত ছোট সীমা নেবেন, তা নির্ভর করে সংখ্যাগুলো কত বড় তার ওপর।
C++20-এর তিন-মুখী তুলনা — <=>
দুটি মান তুলনার আসল প্রশ্ন একটাই: প্রথমটি কি ছোট, সমান, না বড়? C++20 এই তিন উত্তর একবারে দেওয়ার জন্য একটি নতুন অপারেটর এনেছে — three-way comparison a <=> b, যাকে দেখতে মহাকাশযানের মতো বলে ডাকনাম “spaceship operator”. এর ফলকে 0-এর সাথে মেলাতে হয়: (a <=> b) < 0 মানে a ছোট, == 0 মানে সমান, > 0 মানে বড়।
#include <compare>
auto r = 3 <=> 5; // std::strong_ordering::less
bool smaller = (r < 0); // trueসংখ্যার জন্য এটা হয়তো বাড়াবাড়ি মনে হচ্ছে — < তো আছেই। এর আসল শক্তি নিজের টাইপে: একটি <=> লিখলে (বা compiler-কে বানিয়ে নিতে বললে) compiler সেখান থেকেই <, >, <=, >= চারটি বানিয়ে নেয়। আগে ছয়টি অপারেটর আলাদা আলাদা হাতে লিখতে হতো। নিজের টাইপে কীভাবে লিখবেন, তা পর্ব ২৩ ও পর্ব ৪২-এ।
৫ · যুক্তি — &&, ||, ! আর short-circuit
বাস্তবের শর্ত প্রায়ই একাধিক শর্ত জোড়া দিয়ে তৈরি: “নম্বর ৩৩ বা বেশি এবং উপস্থিতি ৭৫% বা বেশি হলে পাস।” এগুলো জোড়ার জন্য তিনটি logical operator:
| অপারেটর | নাম | কখন সত্য | বিকল্প শব্দ |
|---|---|---|---|
a && b | AND (এবং) | দুটোই সত্য হলে | a and b |
a || b | OR (অথবা) | অন্তত একটি সত্য হলে | a or b |
!a | NOT (না) | a মিথ্যা হলে | not a |
শেষ কলামটি অনেককে অবাক করে: C++-এ and, or, not ভাষারই অংশ, কোনো header লাগে না। খুব কম কোডে দেখবেন, তবে পড়তে পারা উচিত।
বাস্তব উদাহরণ: অধিবর্ষ
গ্রেগরিয়ান ক্যালেন্ডারে কোনো বছর অধিবর্ষ, যদি তা ৪ দিয়ে বিভাজ্য হয় কিন্তু ১০০ দিয়ে নয় — অথবা ৪০০ দিয়ে বিভাজ্য হয়। তাই ২০২৪ অধিবর্ষ, ১৯০০ নয়, ২০০০ অধিবর্ষ। বাংলা বাক্যটি প্রায় হুবহু C++-এ বসে যায়:
(y % 4 == 0 && y % 100 != 0) || y % 400 == 0Short-circuit — দরকার না হলে ডান পাশ চলে না
&&-এর বাম পাশ মিথ্যা হলে পুরো উত্তর তো মিথ্যাই — ডান পাশ দেখার দরকার কী? C++ ঠিক এটাই করে: ডান পাশ চালায়ই না. একইভাবে ||-এর বাম পাশ সত্য হলে ডান পাশ বাদ। একে বলে short-circuit evaluation, আর C++ নিশ্চয়তা দেয় যে বাম পাশ সবসময় আগে, পুরোপুরি শেষ হয়ে তারপর ডান পাশ (যদি দরকার হয়)। এটা শুধু গতি বাড়ানোর কৌশল নয় — এটা দিয়ে বিপজ্জনক কাজ আটকানো যায়:
1int count = 0, total = 500;2if (count != 03 && total / count > 50)4 std::println("high average");5else std::println("skipped");6if (total / count > 50 && count != 0)
| count | |
| count != 0 | |
| total / count |
- শুরু। গড় বের করতে হবে, কিন্তু এখনো কোনো ছাত্র নেই —
countশূন্য। সরাসরিtotal / countকরলে শূন্য দিয়ে ভাগ। - বাম পাশ আগে।
count != 0হিসাব হলো — মিথ্যা।&&-এর একটি পাশ মিথ্যা হলেই পুরোটা মিথ্যা, তাই উত্তর এখনই জানা। - ডান পাশ বাদ।
total / countচালানোই হয়নি — ভাগের চেষ্টাই হলো না, তাই কোনো বিপদ নেই।elseঅংশে গিয়ে “skipped” ছাপা হলো। বাম পাশটি যেন দরজার পাহারাদার: সে না বললে ভেতরে কেউ ঢোকে না। - এবার ৪ জন ছাত্র।
count= 4, বাম পাশ সত্য। এখন উত্তর ডান পাশের ওপর নির্ভর করে, তাই ডান পাশ চলে: 500 / 4 = 125, আর 125 > 50 সত্য। - দুটোই সত্য। পুরো শর্ত সত্য, “high average” ছাপা হলো। দরকারের সময় হিসাব হয়েছে, বিপদের সময় হয়নি।
- উল্টো ক্রমে লিখলে?
total / count > 50 && count != 0— একই দুটি শর্ত, শুধু জায়গা বদল। কিন্তু এখন ভাগ আগে হয়, পাহারাদার পরে আসে।countশূন্য হলে ভাগ হয়েই যায় — undefined behavior, Linux-এ সাধারণত প্রোগ্রাম এমন বার্তা দিয়ে ভেঙে পড়ে।&&-এ ক্রমই নিরাপত্তা।
একই কৌশল সবখানে: i < v.size() && v[i] == target — আগে সীমা যাচাই, তারপর পড়া; p != nullptr && p->balance > 0 — আগে দেখে নেওয়া যে ঠিকানা আছে (pointer, পর্ব ১৫)। আর ||-এ: isCached(id) || loadFromServer(id) — ক্যাশে থাকলে সার্ভারে যাওয়াই হয় না।
bool আর int — পুরোনো আত্মীয়তা
C++-এ bool নিজস্ব টাইপ, কিন্তু সংখ্যার সাথে এর সম্পর্ক ঘনিষ্ঠ — এটা C থেকে আসা উত্তরাধিকার। হিসাবে গেলে true হয় 1, false হয় 0 — তাই true + true = 2 (একটি int). উল্টো দিকে, শর্তের জায়গায় যেকোনো সংখ্যা বসানো যায়: শূন্য মানে মিথ্যা, শূন্য ছাড়া সব (−৫ সহ) মানে সত্য। এই নিঃশব্দ বদলটি এক ধরনের implicit conversion. তাই if (count) মানে if (count != 0).
সুবিধাজনক, কিন্তু স্পষ্ট নয়। নতুন কোডে if (count != 0) লেখাই ভালো — পাঠক বোঝেন আপনি সংখ্যা যাচাই করছেন, আর if (count = 0)-এর মতো ভুল চোখে পড়ে। আরেকটি ছোট তথ্য: std::println("{}", true) ছাপে true, কিন্তু std::cout << true ছাপে 1 (যদি না std::boolalpha দেন) — পরের পর্বে এ নিয়ে আরও।
এবার অধিবর্ষ, short-circuit আর নিরাপদ signed-unsigned তুলনা — তিনটি একসাথে চালিয়ে দেখুন:
#include <initializer_list>
#include <print>
#include <utility>
bool isLeap(int y) {
return (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
}
int main() {
for (int y : {1900, 2000, 2024, 2026})
std::println("{}: {}", y, isLeap(y) ? "leap year" : "not leap");
int count = 0, total = 500;
if (count != 0 && total / count > 50) std::println("high average");
else std::println("count is 0, division skipped");
unsigned int size = 3;
std::println("-1 < size? {}", std::cmp_less(-1, size));
}প্রথম চার লাইন: ১৯০০ “not leap” (১০০ দিয়ে বিভাজ্য, ৪০০ দিয়ে নয়), ২০০০ আর ২০২৪ “leap year”, ২০২৬ “not leap”. isLeap একটি ছোট ফাংশন — বিস্তারিত পর্ব ০৯-এ। তারপর count is 0, division skipped — short-circuit ভাগটা হতেই দেয়নি। শেষ লাইন true: std::cmp_less গাণিতিকভাবে তুলনা করে, তাই −১ সত্যিই ৩-এর চেয়ে ছোট। যদি সরাসরি -1 < size লিখতেন, compiler সতর্ক করত আর ফল আসত false.
৬ · Bitwise অপারেটর — প্রতিটি বিট আলাদা করে
এখন পর্যন্ত অপারেটরগুলো পুরো সংখ্যা নিয়ে কাজ করেছে। bitwise operator কাজ করে সংখ্যার ভেতরের প্রতিটি বিট নিয়ে, আলাদা আলাদা করে: & (দুটি বিটই 1 হলে 1), | (যেকোনো একটি 1 হলে 1), ^ (ঠিক একটি 1 হলে 1, XOR), ~ (সব বিট উল্টে দেওয়া), আর <<, >> (বিটগুলো বামে বা ডানে সরানো)।
& আর && সম্পূর্ণ ভিন্ন! && দুটি সত্য/মিথ্যা মান জোড়ে আর short-circuit করে; & দুটি সংখ্যার বিট মেলায় আর দুই পাশই সবসময় চালায়। 2 && 1 হলো true, কিন্তু 2 & 1 হলো 0 (বাইনারি 10 আর 01-এ কোনো মিল নেই)।
বাস্তবে এর একটি সুন্দর ব্যবহার — অনেকগুলো হ্যাঁ/না তথ্য একটি ছোট সংখ্যায় রাখা। প্রতিটি বিট একটি “সুইচ”। এমন সংখ্যাকে বলে bitmask. Linux-এ ফাইলের read/write/execute অনুমতি ঠিক এভাবেই রাখা হয়। ধাপে ধাপে দেখুন:
READ = 1 = 001 WRITE = 2 = 010 EXEC = 4 = 100 একই কথা শিফট দিয়ে: 1u << 0, 1u << 1, 1u << 2
001 (READ)
| 010 (WRITE)
---
011 perm = 3
011 & 010 = 010 -> not 0 -> WRITE allowed 011 & 100 = 000 -> 0 -> EXEC denied
~010 = 101 (WRITE ছাড়া বাকি সব 1) 011 & 101 = 001 perm = 1 (শুধু READ)
001 ^ 100 = 101 EXEC চালু (perm = 5) 101 ^ 100 = 001 আবার চালালে বন্ধ Linux: chmod 754 = rwx r-x r-- (7 = 111, 5 = 101, 4 = 100)
- প্রতিটি অনুমতির একটি বিট। READ ডানের প্রথম বিট, WRITE দ্বিতীয়, EXEC তৃতীয়। এজন্য মানগুলো ১, ২, ৪ — দুইয়ের ঘাত, যাতে কারও বিট অন্যের সাথে না মেলে। (
int-এ আসলে ৩২টি বিট, এখানে শুধু ডানের তিনটি দেখানো হচ্ছে।) |দিয়ে চালু। READ আর WRITE দুটোই দিতে চাইলেREAD | WRITE: যে ঘরে যেকোনো একটিতে 1, ফলেও 1. ফল 011 = 3 — একটি সংখ্যায় দুটি অনুমতি।&দিয়ে যাচাই। WRITE আছে কি না জানতেperm & WRITE: শুধু WRITE-এর ঘরটি রেখে বাকি সব শূন্য হয়ে যায়। ফল শূন্য না হলে অনুমতি আছে। বন্ধনী খেয়াল করুন —(perm & WRITE) != 0; কেন জরুরি, পরের অংশে।~আর&দিয়ে বন্ধ।~WRITEসব বিট উল্টে দেয়: WRITE-এর ঘর 0, বাকি সব 1. এবার&করলে শুধু WRITE-এর বিট মুছে যায়, অন্যগুলো যেমন ছিল তেমনই থাকে।^দিয়ে উল্টানো। XOR একটি বিটকে টগল করে — চালু থাকলে বন্ধ, বন্ধ থাকলে চালু, যেন লাইটের সুইচ। Linux-এরchmod 754-এর প্রতিটি অঙ্ক ঠিক এমন তিন বিটের একটি bitmask.
Bitwise কাজে সবসময় unsigned টাইপ নিন — signed সংখ্যার চিহ্ন-বিট নিয়ে নানা সূক্ষ্মতা আছে। আর বিটের সংখ্যার সমান বা বেশি ঘর শিফট করা (যেমন ৩২-বিট int-এ x << 32) undefined behavior. বিট নিয়ে পুরো গভীর আলোচনা — মাস্ক, শিফট, বিট গোনা — C কোর্সের bitwise operation পর্বে আছে; নিয়মগুলো C++-এও একই। আধুনিক C++-এ std::bitset আর C++20-এর <bit> header-ও আছে (<bit>-এর পরিচয় পর্ব ৪১-এর লাইব্রেরি ভ্রমণে)।
৭ · Precedence আর associativity — কোনটা আগে
স্কুলে শিখেছেন “ভাগ-গুণ আগে, যোগ-বিয়োগ পরে”। C++-এও এমন নিয়ম আছে, শুধু অপারেটর অনেক বেশি। কোন অপারেটর আগে তার operand-গুলোকে “আঁকড়ে ধরে”, তার নাম precedence. আর একই স্তরের অপারেটর পাশাপাশি থাকলে বাম থেকে না ডান থেকে জোড়া হবে, তার নাম associativity. যেমন 10 - 4 - 3 = (10 - 4) - 3 = 3 (বাম থেকে), কিন্তু a = b = 0 = a = (b = 0) (ডান থেকে)।
| স্তর (উচ্চ → নিম্ন) | অপারেটর | জোড়ার দিক |
|---|---|---|
| ১ | a++ a-- f() a[i] a.b | বাম → ডান |
| ২ | ++a --a ! ~ unary - +, cast | ডান → বাম |
| ৩ | * / % | বাম → ডান |
| ৪ | + - | বাম → ডান |
| ৫ | << >> | বাম → ডান |
| ৬ | <=> (C++20) | বাম → ডান |
| ৭ | < <= > >= | বাম → ডান |
| ৮ | == != | বাম → ডান |
| ৯ | &, তারপর ^, তারপর | | বাম → ডান |
| ১০ | &&, তারপর || | বাম → ডান |
| ১১ | ? : = += -= ইত্যাদি | ডান → বাম |
| ১২ | , (comma) | বাম → ডান |
পুরো টেবিল মুখস্থের দরকার নেই। তিনটি জিনিস মনে রাখলেই চলে: গণিত তুলনার আগে, তুলনা &&/||-এর আগে, আর assignment প্রায় সবার শেষে। এবার একটি মিশ্র expression ধাপে ধাপে ভাঙা যাক:
5 + 3 * 2 > 10 && 8 % 3 == 2 সবচেয়ে উঁচু স্তর: * আর %
5 + 6 > 10 && 2 == 2 এবার +
11 > 10 && 2 == 2 এবার > (ছোট-বড় তুলনা, == এর চেয়ে উঁচু)
true && 2 == 2 এবার ==
true && true এবার &&
ok = true পুরো কাঠামো: ((5 + (3 * 2)) > 10) && ((8 % 3) == 2)
- সবচেয়ে উঁচু স্তর আগে জোড়া পায়। লাইনে আছে
+ * > && % ==আর=. এদের মধ্যে*আর%সবচেয়ে উঁচু স্তরে (৩), তাই3 * 2আর8 % 3আগে জোট বাঁধে। - তারপর যোগ।
3 * 2= 6,8 % 3= 2. এখন+(স্তর ৪) বাকিদের চেয়ে উঁচু:5 + 6. - ছোট-বড় তুলনা।
11 > 10— স্তর ৭, যা==-এর (স্তর ৮) চেয়ে উঁচু। ফলtrue. - সমতা। এবার
2 == 2—true. খেয়াল করুন, তুলনাগুলো সব&&-এর আগে শেষ হলো, তাই বন্ধনী ছাড়াই শর্তটি স্বাভাবিকভাবে পড়া যায়। - যুক্তি।
true && true=true.&&নিচের দিকের স্তরে, যাতে দুই পাশের পুরো তুলনা আগে শেষ হয়। - সবশেষে assignment.
=প্রায় সবচেয়ে নিচে, তাই পুরো ডান পাশ শেষ হলে তবেইok-এ মান বসে। মনে রাখুন: precedence ঠিক করে কে কার সাথে জোড়া, কিন্তু কোনটা আগে হিসাব হবে তা সবসময় ঠিক করে না —3 * 2আর8 % 3-এর মধ্যে কোনটা compiler আগে করবে, তা তার ইচ্ছা। শুধু&&,||,?:আর comma-র মতো কিছু অপারেটর ক্রমের নিশ্চয়তা দেয়।
যেখানে precedence ঠকায়
টেবিলের স্তর ৯ খেয়াল করুন: bitwise & তুলনা ==-এর নিচে. তাই জোড় যাচাইয়ের জন্য লেখা x & 1 == 0 আসলে পড়া হয় x & (1 == 0) = x & 0 = 0 — সবসময় মিথ্যা, x যা-ই হোক! এটা C থেকে আসা একটি ঐতিহাসিক দুর্ঘটনা (ভাবনার প্রশ্ন ১-এ গল্পটি আছে)। GCC -Wall-এ এখানে সতর্ক করে: suggest parentheses around comparison in operand of '&'.
আরেকটি: std::cout << a & b; — << (স্তর ৫) &-এর চেয়ে উঁচু, তাই এটা হয় (std::cout << a) & b, যা compile error. std::cout-এর << আসলে শিফটের অপারেটর, শুধু নতুন কাজ দেওয়া (অংশ ৮) — তাই শিফটের precedence-ই পায়।
নিশ্চিত না হলে বন্ধনী দিন। (x & 1) == 0, (a && b) || c, std::cout << (a & b). বন্ধনীর জন্য প্রোগ্রাম এক ন্যানোসেকেন্ডও ধীর হয় না — compiler-এর কাছে শুধু কাঠামো স্পষ্ট হয়। আর পরের পাঠক (ছয় মাস পরের আপনি নিজেও) টেবিল না খুলেই বুঝে ফেলেন। && আর || একসাথে থাকলে বন্ধনী দেওয়া প্রায় সব style guide-এর পরামর্শ।
৮ · Ternary, comma আর নিজের অপারেটর
Ternary ?: — এক লাইনের সিদ্ধান্ত
ternary operator লেখা হয় শর্ত ? মান১ : মান২ — শর্ত সত্য হলে প্রথম মান, মিথ্যা হলে দ্বিতীয়। এটি একটি expression, তাই এর ফল সরাসরি ভ্যারিয়েবলে রাখা যায়, এমনকি const ভ্যারিয়েবলেও: const int fee = isStudent ? 50 : 100;. if-else দিয়ে লিখলে fee-কে আগে খালি বানিয়ে পরে বদলাতে হতো, const করা যেত না।
Ternary ডান থেকে বামে জোড়া হয়, তাই কয়েকটি পরপর বসানো যায়। বাংলাদেশের SSC/HSC গ্রেড পদ্ধতির একটি অংশ:
const char* grade = (marks >= 80) ? "A+"
: (marks >= 70) ? "A"
: (marks >= 60) ? "A-"
: (marks >= 33) ? "pass"
: "F";এভাবে সাজিয়ে লিখলে টেবিলের মতো পড়া যায়। কিন্তু এক লাইনে তিন-চারটি ternary গুঁজে দিলে কেউ পড়তে পারবে না — তখন if-else ভালো (পর্ব ০৮)। দুটি শাখার টাইপ একই রকম রাখুন; একটি int আর অন্যটি double হলে পুরো ফল double হয়ে যায়।
Comma অপারেটর — কম দেখবেন
comma operator a, b আগে a চালায়, তার ফল ফেলে দেয়, তারপর b চালিয়ে b-এর মান দেয়। প্রায় একমাত্র জায়গা যেখানে এটা স্বাভাবিক, তা হলো for লুপে দুটি ভ্যারিয়েবল একসাথে বদলানো: for (int i = 0, j = n - 1; i < j; ++i, --j) — এখানে ++i, --j comma অপারেটর। (int i = 0, j = n - 1-এর comma কিন্তু অপারেটর নয়, শুধু একাধিক ভ্যারিয়েবল ঘোষণার চিহ্ন।)
অন্য জায়গায় এটা মূলত বিভ্রান্তি তৈরি করে। int x = (1, 2); — x হয় 2. আর arr[i, j] লিখে অনেকে ভাবতেন দুই-মাত্রার array-র ঘর পড়ছেন, আসলে পড়তেন arr[j]. এই কারণে C++20 []-এর ভেতরে comma অপারেটর ব্যবহার deprecated করে, আর C++23 এই লেখাটিকে নতুন অর্থ দেয় — একাধিক সূচক নেওয়া operator[].
নিজের টাইপে অপারেটর — একটি ঝলক
একটি প্রশ্ন ভাবুন: std::string তো ভাষার মৌলিক টাইপ নয়, লাইব্রেরির একটি class. তাহলে "Dhaka"s + " Metro"-তে + কীভাবে কাজ করে? আর std::cout << x-এ শিফটের << কীভাবে ছাপার কাজ করে? উত্তর: C++-এ নিজের টাইপের জন্য অপারেটরের নতুন অর্থ দেওয়া যায় — একে বলে operator overloading. পর্ব ২৩-এ আপনি নিজের Money টাইপে + আর == লিখবেন। দুটি নিয়ম এখনই জেনে রাখুন: precedence বদলানো যায় না (নিজের <<-ও শিফটের স্তরেই থাকে), আর নিজের লেখা && বা || short-circuit করে না — তাই এ দুটি প্রায় কেউ overload করেন না।
গণিত, তুলনা, যুক্তি, bitwise আর precedence — প্রায় সবই C-এর হুবহু এক, এমনকি &-এর পুরোনো precedence-এর ফাঁদও। পার্থক্য চারটি: C++-এ bool, true, false আর and/or/not ভাষারই অংশ (C-তে এর জন্য <stdbool.h> ও <iso646.h> লাগে, যদিও C23 থেকে bool, true, false keyword); C++20-এ <=> আছে, C-তে নেই; নিজের টাইপে অপারেটর overload করা যায়; আর C++17 কিছু expression-এর হিসাবের ক্রম নিশ্চিত করেছে (যেমন i = i++), যা C-তে এখনো undefined. গভীর আলোচনার জন্য C কোর্সের অপারেটর আর bitwise operation পর্ব দেখুন।
i++আর++i-এর পার্থক্য? —i++পুরোনো মান দেয়, তারপর বাড়ায়;++iআগে বাড়িয়ে নতুন মান দেয়। একা লাইনে দুটি একই।-7 / 2আর-7 % 2কত? —-3আর-1; C++11 থেকে ভাগফল শূন্যের দিকে কাটে, ভাগশেষের চিহ্ন ভাজ্যের মতো।- Short-circuit evaluation কী? —
&&-এর বাম পাশ মিথ্যা বা||-এর বাম পাশ সত্য হলে ডান পাশ চালানোই হয় না। if (x = 5)কী করে? —x-এ 5 বসায়, আর শর্ত সবসময় সত্য; তুলনার জন্য==লাগে।&আর&&-এর পার্থক্য? —&বিট ধরে মেলায় আর দুই পাশই চালায়;&&সত্য/মিথ্যা জোড়ে আর short-circuit করে।
৯ · AI-এর যুগে — অপারেটরের ভুল চোখে ধরা
অপারেটরের ভুলগুলো ছোট — এক অক্ষর, এক বন্ধনী। আর ঠিক এই কারণে বিপজ্জনক: কোড compile হয়, বেশিরভাগ ইনপুটে ঠিক চলে, শুধু একটি বিশেষ ক্ষেত্রে (ঋণাত্মক সংখ্যা, শূন্য, খালি তালিকা) ভুল ফল দেয়। AI টুল বিশাল পরিমাণ পুরোনো কোড থেকে শেখে, তাই সেই কোডের প্রচলিত ভুলগুলোও মাঝে মাঝে ফিরিয়ে আনে। AI-এর লেখা কোডে যা খুঁজবেন:
| AI যা লেখে | সমস্যা | যা হওয়া উচিত |
|---|---|---|
if (status = DONE) | তুলনা নয়, assignment — শর্ত প্রায় সবসময় সত্য, মানও মুছে যায় | if (status == DONE) |
if (x & 1 == 0) | x & (1 == 0) — সবসময় মিথ্যা | if ((x & 1) == 0) বা x % 2 == 0 |
for (int i = 0; i < v.size() - 1; ++i) | signed-unsigned মেশানো; খালি v-তে v.size() - 1 বিশাল সংখ্যা হয়ে যায় | i + 1 < v.size(), বা std::ssize(v) |
if (n % 2 == 1) বিজোড় যাচাইয়ে | ঋণাত্মক বিজোড়ে ভাগশেষ -1 — ভুল উত্তর | n % 2 != 0 |
total / count > 50 && count != 0 | পাহারাদার পরে — শূন্য দিয়ে ভাগ আগেই হয়ে যায় | count != 0 && total / count > 50 |
if (0 < x < 10) | গণিতের মতো পড়া হয় না — সবসময় সত্য | 0 < x && x < 10 |
arr[i] = i++; | একই লাইনে বদল আর ব্যবহার — বোঝা কঠিন, পুরোনো standard-এ undefined | দুই লাইনে ভেঙে লেখা |
rate / 100 * price (int) | rate / 100 আগে হয়ে শূন্য | price * rate / 100 |
AI-কে কোড লিখতে বলার সময় নিয়মগুলো আগেই বলে দিন, আর পরে নিজে যাচাই করুন। একটি নমুনা prompt:
AI-এর কোডে অপারেটর যাচাইয়ের চেকলিস্ট
এক অক্ষরের ভুল ধরতে পারাই এখন আসল দক্ষতা।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১x & 1 == 0-এর মতো ফাঁদ তো সবাই জানে — তবু C++ কেন C-এর এই অদ্ভুত precedence বদলায়নি?
গল্পটা শুরু C-এরও আগে। C-এর পূর্বসূরি ভাষা B (আর তারও আগে BCPL)-এ আলাদা && আর || ছিল না। একই & দুই কাজ করত: সংখ্যার মধ্যে bitwise AND, আর if-এর শর্তের ভেতরে “এবং” (short-circuit সহ)। শর্তে if (a == b & c == d) লেখা যেত, আর সেটা স্বাভাবিকভাবে পড়ার জন্য &-কে ==-এর নিচে রাখা হয়েছিল — যুক্তির অপারেটরের জন্য এটাই সঠিক জায়গা।
১৯৭০-এর দশকের শুরুতে Dennis Ritchie C-তে দুটি কাজ আলাদা করলেন — নতুন এল && আর ||. তখন &-কে নিয়মমতো ==-এর ওপরে তুলে দেওয়া উচিত ছিল, কারণ এখন সে শুধুই বিটের অপারেটর। কিন্তু ততদিনে Unix-এর কয়েকশো কিলোবাইট C কোড আর মাত্র কয়েকটি ইনস্টলেশন ছিল, আর সেই কোডে a == b & c == d শর্ত হিসেবে লেখা। Ritchie পরে তাঁর “The Development of the C Language” প্রবন্ধে লিখেছেন, পেছন ফিরে দেখলে precedence বদলানোই ভালো হতো, কিন্তু তখন পুরোনো অপারেটরকে সরানোর চেয়ে শুধু & আর && আলাদা করাই নিরাপদ মনে হয়েছিল। আজকের হিসাবে সেই “পুরোনো কোড” ছিল খুব ছোট — কিন্তু সিদ্ধান্তটা হয়েছিল সেই সময়ের হিসাবে।
C++ যখন ১৯৮০-র দশকে আসে, ততদিনে পৃথিবীতে লাখ লাখ লাইন C কোড। Bjarne Stroustrup-এর নকশার নীতি ছিল “C-এর যত কাছে সম্ভব, কিন্তু তার চেয়ে কাছে নয়”। C++-এর বড় সুবিধাই ছিল যে C কোড প্রায় না বদলে C++ compiler-এ চলে, আর C header সরাসরি ব্যবহার করা যায়। যদি C++ &-এর precedence বদলাত, তাহলে একই লাইন C-তে এক অর্থ আর C++-এ অন্য অর্থ দিত — এবং সবচেয়ে খারাপ ব্যাপার, কোনো error ছাড়া। নিঃশব্দে অর্থ বদলানো ভাষার নকশায় সবচেয়ে বড় পাপগুলোর একটি; compile error অন্তত চোখে পড়ে।
তাই সমাধান এল অন্য পথে — compiler-এর সতর্কবার্তা। GCC আর Clang আজ -Wall-এ -Wparentheses দিয়ে ঠিক এই ধরনের লেখা ধরে আর বন্ধনী দিতে বলে। ভাষা না বদলে, ভুলটা সামনে আনা হলো।
অন্য ভাষাগুলো এই শিক্ষা নিয়েছে। Go, Rust আর Swift-এ bitwise অপারেটর তুলনার ওপরে, তাই x & 1 == 0 সেখানে স্বাভাবিক অর্থেই কাজ করে (বা টাইপের নিয়মে error দেয়)। নতুন ভাষা পুরোনো ভুল ঠিক করতে পারে; পুরোনো ভাষাকে বাঁচতে হয় পুরোনো কোডের সাথে।
মূল উপলব্ধি: জনপ্রিয় ভাষায় একটি নকশার ভুল একবার ঢুকলে প্রায় চিরস্থায়ী হয়ে যায়, কারণ তার ওপর কোটি লাইন কোড দাঁড়িয়ে থাকে। সমাধান তখন ভাষার বাইরে — warning, style guide আর বন্ধনী দেওয়ার অভ্যাস।
প্র ০২ছয়টি তুলনার অপারেটর তো ছিলই — C++20 কেন আলাদা করে <=> আনল?
মৌলিক টাইপে ছয়টি অপারেটরই যথেষ্ট। সমস্যা দেখা দেয় নিজের টাইপে। ধরুন আপনি একটি Date টাইপ বানালেন (বছর, মাস, দিন)। তারিখ সাজাতে বা তুলনা করতে চাইলে C++20-এর আগে ==, !=, <, >, <=, >= — ছয়টি ফাংশন হাতে লিখতে হতো। প্রতিটিতে প্রায় একই যুক্তি: আগে বছর মেলাও, সমান হলে মাস, তারপর দিন। আর ছয়টির মধ্যে একটিতে একটি < ভুল করে <= লিখলে সাজানোর অ্যালগরিদম অদ্ভুত আচরণ করে — এমন বাগ খুঁজে পাওয়া কঠিন। একঘেয়ে, দীর্ঘ, আর ভুল-প্রবণ — একে প্রোগ্রামাররা বলেন boilerplate.
অন্য ভাষায় সমাধান আগেই ছিল। Perl-এ সংখ্যা তুলনার <=> অপারেটর বহু বছর ধরে আছে, আর Ruby-তে একটি <=> লিখলেই বাকি তুলনাগুলো পাওয়া যায়। Java-র compareTo বা C-এর strcmp-ও একই ধারণা: একবার তুলনা করে ঋণাত্মক, শূন্য বা ধনাত্মক উত্তর।
২০১৭ সালে Herb Sutter C++-এর জন্য “Consistent comparison” (P0515) প্রস্তাব দেন, যা C++20-এ গৃহীত হয়। এতে তিনটি বড় ধারণা ছিল। প্রথমত, একটি অপারেটর থেকে চারটি: <=> থাকলে compiler নিজেই a < b-কে (a <=> b) < 0 হিসেবে লিখে নেয়। দ্বিতীয়ত, default: auto operator<=>(const Date&) const = default; লিখলে compiler সদস্যগুলো ক্রমানুসারে তুলনা করার কোড নিজেই বানায় — এক লাইনে সব শেষ। তৃতীয়ত, তুলনার ধরনকে টাইপে প্রকাশ: ফল হতে পারে std::strong_ordering (পূর্ণসংখ্যার মতো, সমান মানে সত্যিই অভিন্ন), std::weak_ordering (যেমন বড়-ছোট হাতের অক্ষর না মেনে নাম তুলনা — “Rahim” আর “rahim” সমমানের কিন্তু অভিন্ন নয়), বা std::partial_ordering (দশমিক সংখ্যা, যেখানে NaN কারও সাথেই তুলনীয় নয়)। আগে এই পার্থক্যগুলো শুধু ডকুমেন্টেশনে লেখা থাকত; এখন compiler-ও জানে।
পরে একটি সংশোধনও হয়েছে: == আলাদাই রাখা হলো, কারণ string-এর মতো টাইপে “সমান কি না” দেখা প্রায়ই অনেক দ্রুত (আগে দৈর্ঘ্য মিলিয়ে দেখলেই হয়), কিন্তু পূর্ণ ক্রম বের করতে প্রতিটি অক্ষর দেখতে হয়।
মূল উপলব্ধি: ভালো ভাষা-নকশা পুনরাবৃত্তি কমায়, কারণ যেখানে একই যুক্তি ছয়বার লিখতে হয়, সেখানে ছয়বার ভুলের সুযোগ। <=> তুলনার যুক্তিকে এক জায়গায় এনে নির্ভুলতাকে compiler-এর দায়িত্ব বানিয়েছে।
প্র ০৩Short-circuit কি শুধু গতি বাড়ানোর কৌশল? নাকি এটা আসলে একটি “লুকোনো if”?
প্রথম দেখায় মনে হয় optimization — উত্তর জানা হয়ে গেলে বাকি কাজ বাদ। কিন্তু C++ standard এটাকে ঐচ্ছিক রাখেনি, নিশ্চয়তা দিয়েছে: &&-এর বাম পাশ মিথ্যা হলে ডান পাশ চলবে না, আর বাম পাশের সব কাজ (side effect সহ) ডান পাশ শুরুর আগেই শেষ হবে। যে আচরণের ওপর আপনি নিরাপত্তার জন্য নির্ভর করতে পারেন, সেটা আর optimization নয় — সেটা control flow. a && b আসলে if (a) { return b; } else { return false; }-এর সংক্ষিপ্ত রূপ।
এই ধারণার শিকড় পুরোনো। ১৯৬০-এর আশপাশে John McCarthy Lisp-এর জন্য conditional expression-এর ধারণা দেন, যেখানে শর্তের যুক্তি ধাপে ধাপে, দরকার হলে তবেই হিসাব হয়। C-তে Ritchie যখন && আর || আলাদা করেন (প্রশ্ন ১), তখন short-circuit-কে তাদের নিশ্চিত আচরণ বানান। অন্য অনেক ভাষা এখানে ভিন্ন পথ নিয়েছিল — যেমন Pascal-এর মূল সংজ্ঞায় and দুই পাশই হিসাব করতে পারত, তাই সেখানে (p <> nil) and (p^.x > 0) নিরাপদ ছিল না; পরে অনেক Pascal compiler short-circuit-কে আলাদা সুবিধা হিসেবে যোগ করে।
আজ এই “লুকোনো if” সব জায়গায়। Shell script-এ make && ./app — build সফল হলে তবেই চালাও। Perl-এ বিখ্যাত open(...) or die. JavaScript-এ user && user.name. C++-এ p && p->ok(), i < n && a[i] > 0.
তবে এর দুটি দাম আছে। এক, ক্রমের নিশ্চয়তা মানে compiler দুই পাশ উল্টে দিতে পারে না, আর প্রতিটি && মেশিন কোডে একটি branch (শাখা) হতে পারে — আধুনিক CPU-তে ভুল অনুমান করা branch ব্যয়বহুল। তাই দুই পাশে side effect না থাকলে compiler অনেক সময় branch ছাড়া কোড বানায়, আর পারফরম্যান্স-সচেতন কোডে কখনো কখনো জেনেশুনে & ব্যবহার করা হয় (পর্ব ৪৯)। দুই, নিজের টাইপে && overload করলে সেটা সাধারণ ফাংশন হয়ে যায় — দুটি argument-ই আগে হিসাব হয়, short-circuit হারায়। এজন্যই Scott Meyers-এর More Effective C++-সহ অনেক বই আর style guide এগুলো overload না করতে বলে।
মূল উপলব্ধি: && আর || শুধু যুক্তির অপারেটর নয়, ক্রম-নিয়ন্ত্রণের যন্ত্র। পাহারাদার শর্তকে বাম পাশে রাখা একটি নিরাপত্তা-নকশা, আর পাশ দুটি অদলবদল করা মানে প্রোগ্রামের অর্থ বদলানো।
প্র ০৪i = i++ একসময় undefined behavior ছিল, C++17-এ হলো সংজ্ঞায়িত — কী বদলাল, আর কেন i++ + i++ এখনো undefined?
C আর পুরোনো C++ “sequence point” দিয়ে ক্রম বোঝাত — প্রোগ্রামের এমন বিন্দু, যেখানে আগের সব side effect শেষ হতেই হবে (যেমন ;, &&, ||, comma). দুটি sequence point-এর মাঝে একই ভ্যারিয়েবল দুবার বদলালে, বা বদলে আবার অন্য কাজে পড়লে, আচরণ undefined. i = i++-এ মাঝে কোনো sequence point নেই — ++-এর বদল আর =-এর বদল একই ভ্যারিয়েবলে, কোনো ক্রম ছাড়া। তাই undefined.
কেন এত ঢিলেঢালা? ১৯৭০-৮০-র দশকের compiler-কে যতটা সম্ভব স্বাধীনতা দেওয়া, যাতে সে নিজের মেশিনের জন্য সেরা ক্রমে নির্দেশ সাজাতে পারে। কিছু calling convention-এ function argument ডান থেকে বামে stack-এ তোলা হতো, তাই ডান দিক আগে হিসাব করা সুবিধাজনক ছিল।
C++11 পুরো মডেলটি নতুন করে লেখে — multithreading-এর জন্য (পর্ব ৪৫) একটি সঠিক memory model দরকার ছিল, তাই “sequence point”-এর বদলে এল আরও নিখুঁত সম্পর্ক “sequenced before”. তবে বেশিরভাগ ক্ষেত্রে ফল একই রইল।
বড় বদল আসে C++17-এ, P0145 প্রস্তাবে (Gabriel Dos Reis, Herb Sutter, Jonathan Caves), শিরোনাম “Refining Expression Evaluation Order for Idiomatic C++”. প্রস্তাবের অনুপ্রেরণা ছিল একটি বাস্তব উদাহরণ: Stroustrup-এর নিজের বই The C++ Programming Language-এর চতুর্থ সংস্করণে s.replace(...).replace(s.find(...), ...) ধরনের একটি chained কোড ছিল, যা বিশেষজ্ঞদের চোখ এড়িয়ে গিয়েছিল, অথচ পুরোনো নিয়মে এর ফল নিশ্চিত ছিল না — কারণ s.find(...) প্রথম replace-এর আগে না পরে চলবে, তা ঠিক করা ছিল না। বিশেষজ্ঞরাই যদি ভুল করেন, তাহলে নিয়মটাই সমস্যা।
C++17 কয়েকটি ক্রম নিশ্চিত করে: assignment-এর ডান পাশ বাম পাশের আগে (তাই i = i++-এ i++ পুরো শেষ হয়, তারপর পুরোনো মানটি i-তে বসে — i বদলায় না), << ও >>-এর বাম পাশ আগে (তাই cout << i << i++ নিশ্চিত), a.b আর a[b]-এ a আগে। আর function argument-গুলো একে অন্যের সাথে আর মিশে যায় না — কোনটা আগে তা অনির্দিষ্ট, কিন্তু একটি পুরো শেষ হয়ে তবে অন্যটি।
কিন্তু +, *-এর মতো সাধারণ binary অপারেটরের দুই পাশের ক্রম এখনো ঠিক করা হয়নি। তাই i++ + i++-এ দুটি বদলের মধ্যে কোনো ক্রম নেই — এখনো undefined. সব ক্রম বেঁধে দিলে optimizer-এর স্বাধীনতা কমত, আর কমিটি শুধু সেই জায়গাগুলোই ঠিক করেছে, যেখানে প্রচলিত, স্বাভাবিক কোড ভাঙছিল।
মূল উপলব্ধি: “সংজ্ঞায়িত” মানে “ভালো কোড” নয়। i = i++ এখন বৈধ, কিন্তু কোনো কাজ করে না আর পাঠককে বিভ্রান্ত করে; আর C-তে এটি এখনো undefined. এক লাইনে একটি বদল — এই অভ্যাস সব standard-এ নিরাপদ।
প্র ০৫C++11 কেন ঋণাত্মক সংখ্যার ভাগকে “শূন্যের দিকে কাটা” বানাল, যেখানে Python বেছে নিয়েছে floor? কোনটা “সঠিক”?
C89 আর C++98-এ ঋণাত্মক operand থাকলে / আর %-এর ফল ছিল implementation-defined: -7 / 2 হতে পারত -3 বা -4, শুধু (a / b) * b + a % b == a সম্পর্কটি সত্য থাকতে হতো। কারণ ছিল হার্ডওয়্যার — ভিন্ন CPU ভিন্নভাবে ভাগ করত, আর standard চেয়েছিল প্রতিটি মেশিনে ভাগটা একটি মাত্র নির্দেশে হোক।
সময়ের সাথে বাস্তবতা এক দিকে ঝুঁকে পড়ে। Intel x86-এর IDIV নির্দেশসহ প্রায় সব জনপ্রিয় প্রসেসর শূন্যের দিকে কাটে। Fortran-ও বহু দশক ধরে একই নিয়ম মানত, আর বৈজ্ঞানিক কোড Fortran থেকে C-তে অনুবাদের সময় একই ফল পাওয়া জরুরি ছিল। তাই C99 নিয়মটিকে বাধ্যতামূলক করে: শূন্যের দিকে কাটা। C++ ভাষাটি তখন C99-এর সাথে মিলিয়ে নিচ্ছিল, তাই C++11-ও একই নিয়ম লিখে দেয়। এখন -7 / 2 সবখানে -3, আর ভাগশেষের চিহ্ন সবখানে ভাজ্যের মতো।
Python উল্টো পথ নিয়েছে: -7 // 2 = -4, -7 % 2 = 1. Guido van Rossum তাঁর এক লেখায় এর কারণ ব্যাখ্যা করেছেন: গণিতের দৃষ্টিতে ভাগশেষের চিহ্ন ভাজকের মতো হলে (ধনাত্মক ভাজকে সবসময় ০ থেকে ভাজক−১) অনেক হিসাব সহজ হয়। ঘড়ি বা ক্যালেন্ডারের কথা ভাবুন: Unix সময় থেকে দিনের ভেতরে কত সেকেন্ড পেরিয়েছে, তা বের করতে t % 86400; ১৯৭০-এর আগের (ঋণাত্মক) সময়েও Python-এ ফল ধনাত্মক আর অর্থপূর্ণ, কিন্তু C++-এ ঋণাত্মক — আলাদা করে সামলাতে হয়।
তাহলে কোনটা সঠিক? দুটোই একটি সুসংগত নিয়ম, শুধু ভিন্ন অগ্রাধিকার। শূন্যের দিকে কাটার সুবিধা: -(a / b) == (-a) / b — চিহ্ন বদলালে ফলের শুধু চিহ্ন বদলায়, যা অনেকের কাছে স্বাভাবিক, আর হার্ডওয়্যারের সাথে মেলে বলে দ্রুত। Floor-এর সুবিধা: ভাগশেষ সবসময় “চক্রের” ভেতরে থাকে, তাই ঘড়ি, ক্যালেন্ডার, হ্যাশ টেবিলের সূচক বা বৃত্তাকার buffer-এ সরাসরি কাজে লাগে। Python গতির চেয়ে গাণিতিক সুবিধাকে, C/C++ হার্ডওয়্যারের সাথে মিলকে অগ্রাধিকার দিয়েছে।
বাস্তব শিক্ষা: ভাষা বদলালে এই আচরণ বদলায়, আর অনুবাদের সময় বাগ ঢোকে। C++-এ সবসময় ধনাত্মক ভাগশেষ চাইলে নিজে লিখুন ((a % m) + m) % m, আর বিজোড় যাচাইয়ে != 0.
মূল উপলব্ধি: “অঙ্কের সঠিক উত্তর” বলে একটিমাত্র জিনিস সবসময় থাকে না — থাকে একটি সংজ্ঞা আর তার trade-off. যে ভাষায় লিখছেন, তার সংজ্ঞাটি জানুন; বিশেষ করে ঋণাত্মক সংখ্যা আর শূন্যের প্রান্তে।
অনুশীলন
বেশিরভাগ কাজে কোড লিখতে হবে না — আগে অনুমান করুন, তারপর উত্তর মিলিয়ে নিন।
-
মান অনুমান: প্রতিটির মান ও টাইপ বলুন —
17 / 5,17 % 5,-17 / 5,-17 % 5,17.0 / 5,5 / 2 * 2.0,true + true.3(int),2(int),-3(শূন্যের দিকে কাটা),-2(চিহ্ন ভাজ্যের মতো; যাচাই: −৩ × ৫ + (−২) = −১৭),3.4(double),4.0— বাম থেকে ডানে: আগে5 / 2= 2 (int), তারপর 2 × 2.0 = 4.0 (double); দশমিক আগেই হারিয়েছে। শেষেরটি2, টাইপint— হিসাবেboolint-এ উন্নীত হয়। -
বাগ শিকার: তিনটি শর্তের প্রতিটিতে ভুল কী, আর ঠিক রূপ কী? (ক)
if (x & 1 == 0)জোড় যাচাইয়ে (খ)if (attempts = 3)(গ)bool valid = 0 < age < 120;(ক)
==আগে জোড়া পায়, তাইx & (1 == 0)=x & 0= 0 — সবসময় মিথ্যা। ঠিক:(x & 1) == 0. (খ) assignment —attempts-এ 3 বসে, আর শর্ত সবসময় সত্য। ঠিক:attempts == 3. (গ)(0 < age) < 120— বাম পাশ 0 বা 1, যা সবসময় 120-এর চেয়ে ছোট; ফলেage= 500 হলেও সত্য। ঠিক:0 < age && age < 120. তিনটিই-Wallদিলে GCC-তে-Wparenthesesসতর্কবার্তা দেয় (Clang-ও নিজের ভাষায় সতর্ক করে) — তৃতীয়টির বার্তা:comparisons like 'X<=Y<=Z' do not have their mathematical meaning. এজন্যই warning কখনো উপেক্ষা করবেন না। -
Short-circuit-এর ক্রম:
std::vector<int> vখালি। নিচের দুটি শর্তে কী হবে? (ক)if (!v.empty() && v[0] > 0)(খ)if (v[0] > 0 && !v.empty()). আরif (isCached(id) || loadFromServer(id))-এ ক্যাশে থাকলে কয়টি ফাংশন চলে?(ক) নিরাপদ:
v.empty()সত্য, তাই!v.empty()মিথ্যা —v[0]পড়াই হয় না, শর্ত মিথ্যা। (খ) বিপজ্জনক: আগে খালি vector-এরv[0]পড়া হয় — সীমার বাইরে, undefined behavior. পরে পাহারাদার এসে লাভ নেই।||-এর উদাহরণে ক্যাশে থাকলে বাম পাশ সত্য, তাই শুধুisCachedচলে;loadFromServerচলেই না। -
গ্রেড ক্যালকুলেটর (কোড): একটি
int marksথেকে বাংলাদেশের SSC/HSC গ্রেড বের করুন: ৮০–১০০ A+, ৭০–৭৯ A, ৬০–৬৯ A-, ৫০–৫৯ B, ৪০–৪৯ C, ৩৩–৩৯ D, ০–৩২ F. ১০০-এর বেশি বা ঋণাত্মক নম্বরে"invalid"দিন। শুধু এই পর্বের অপারেটর (তুলনা,&&/||, ternary) ব্যবহার করুন।একটি সমাধান:
const char* g = (marks < 0 || marks > 100) ? "invalid" : (marks >= 80) ? "A+" : (marks >= 70) ? "A" : (marks >= 60) ? "A-" : (marks >= 50) ? "B" : (marks >= 40) ? "C" : (marks >= 33) ? "D" : "F";— প্রতিটি লাইনে আলাদা করে সাজিয়ে লিখুন। লক্ষ করুন, প্রথমে অবৈধ নম্বর বাদ দেওয়া হলো (পাহারাদার আগে), তারপর ওপর থেকে নিচে — তাই প্রতি ধাপে শুধু নিচের সীমা দেখলেই চলে। -১, ০, ৩২, ৩৩, ৭৯, ৮০, ১০০, ১০১ দিয়ে পরীক্ষা করুন — সীমার মানগুলোতেই বেশিরভাগ ভুল থাকে। পর্ব ০৮-এ একই কাজif-elseদিয়ে লিখবেন।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএই C++ অপারেটরগুলোকে সবচেয়ে উঁচু precedence থেকে সবচেয়ে নিচু অনুযায়ী সাজান (▲▼ চেপে সরান)
*— গুণ+— যোগ<— ছোট==— সমান&&— যুক্তির এবং=— assignment
C++11 বা পরের standard-এ -7 % 3-এর মান কত?
- 2
- -1
- 1
- -2
C++-এ x & 1 == 0 expression-টি compiler কীভাবে জোড়া দেয়?
- (x & 1) == 0
- (x && 1) == 0
- x & (1 == 0)
- compile error হয়
C++-এ int i = 5; int a = i++; চালানোর পর a আর i-এর মান কত?
- a = 5, i = 6
- a = 6, i = 6
- a = 5, i = 5
- undefined behavior
C++-এ p একটি nullptr হলে p != nullptr && p->balance > 0 হিসাব করলে কী হয়?
- প্রোগ্রাম ভেঙে পড়ে, কারণ nullptr-এর balance পড়া হয়
- compiler যেকোনো পাশ আগে হিসাব করতে পারে
- ফল true
- ফল false, আর p->balance পড়াই হয় না
তিন-মুখী তুলনার অপারেটর <=> (spaceship operator) C++-এর কোন standard-এ যোগ হয়েছে?
- C++11
- C++17
- C++20
- C++23
C++-এ std::vector<int> v(3); int i = -1; হলে i < v.size()-এর ফল কী?
- true, কারণ -1 ছোট
- false, কারণ -1 বিশাল unsigned সংখ্যায় বদলে যায়
- compile error
- undefined behavior
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- অপারেটরের ফল operand-এর টাইপে নির্ভর করে:
int / intদশমিক কাটে; ঋণাত্মকে শূন্যের দিকে, তাই-7 % 2=-1— বিজোড় যাচাইয়ে!= 0. i++পুরোনো মান,++iনতুন মান। এক লাইনে একটি ভ্যারিয়েবল একবারই বদলান।=বসায়,==মেলায়। signed-unsigned মেশাবেন না; দশমিকে==নয়; C++20-এ<=>.&&/||short-circuit করে — পাহারাদার শর্ত (শূন্য, সীমা,nullptr) সবসময় বাম পাশে।- Precedence-এ সন্দেহ হলে বন্ধনী — বিশেষ করে
&|^-এর সাথে তুলনা মিশলে। আর-Wall -Wextraচালু রাখুন।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ০৭ · ইনপুট-আউটপুট — cin, cout, format আর printপরবর্তী পর্বব্যবহারকারীর কাছ থেকে মান নেওয়া আর সুন্দর করে ছাপানো।
- C++ প্রোগ্রামিং · কোর্স হোম৫৫টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- C প্রোগ্রামিং · অপারেটরগভীরে যানএকই অপারেটরগুলোর C-এর দৃষ্টিতে বিস্তারিত আলোচনা।
- C প্রোগ্রামিং · bitwise operationপাশের ট্র্যাকমাস্ক, শিফট আর বিট নিয়ে কাজের পূর্ণ কৌশল।