Foundationপ্রথম নীতি থেকে
LEVEL 1লেসন ৪/১৪মাঝারি৫৫ মিনিট

Unsigned Integer — শুধু অ-ঋণাত্মক সংখ্যার জগৎ

Unsigned Integers

একটা n-bit unsigned integer আসলে ℤ_(2ⁿ)-এর একটা সদস্য — range, wraparound, carry, আর size_t-এর বিপদ সবকিছুই এই এক লাইনের গণিত থেকে বেরিয়ে আসে।

এই লেসন শেষে আপনি পারবেন

  • একটা n-bit pattern থেকে unsigned মান বের করতে পারবেন, আর 8/16/32/64-bit-এর range মুখস্থ না করেই বের করতে পারবেন
  • unsigned addition ও subtraction কেন এবং কীভাবে ঠিক arithmetic mod 2ⁿ, সেটা প্রমাণসহ ব্যাখ্যা করতে পারবেন
  • carry-out আর overflow-এর সম্পর্ক ব্যাখ্যা করতে পারবেন — unsigned-এ কেন এই দুটো ঠিক একই জিনিস
  • size_t কী, কেন এটা unsigned, আর ঠিক কোথায় এটা বিপজ্জনক হয়ে ওঠে সেটা শনাক্ত করতে পারবেন
  • একটা বাস্তব কোডে unsigned wraparound bug (যেমন `size() - 1` loop) খুঁজে বের করে সঠিকভাবে ঠিক করতে পারবেন
  • কম্পাইলারের integer promotion কীভাবে ছোট unsigned টাইপের arithmetic-কে চমকে দেয় সেটা predict করতে পারবেন

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

একটা প্রশ্ন দিয়ে শুরু করি। নিচের C++ কোডটা একটা vector-এর সব element উল্টো দিকে print করার চেষ্টা করছে:

std::vector<int> v = {10, 20, 30};
for (size_t i = v.size() - 1; i >= 0; i--) {
    std::cout << v[i] << " ";
}

চালিয়ে দেখুন। এটা 30 20 10 ছাপিয়ে থামবে না — এটা ক্র্যাশ করবে অথবা অসীম লুপে ঢুকে যাবে, তারপর হয়তো segmentation fault।

কেন? i >= 0 শর্তটা size_t টাইপের জন্য সবসময় সত্য — কারণ size_t কখনো ঋণাত্মক হতে পারে না। i যখন 0 থেকে আরেকবার কমে, সেটা -1 হয় না, হয় একটা বিশাল সংখ্যা (৬৪-bit-এ প্রায় 18 অঙ্কের একটা সংখ্যা)। Loop চলতেই থাকে, v[i] প্রতিবার memory-র এলোমেলো জায়গায় হাত দেয়, যতক্ষণ না প্রোগ্রাম ক্র্যাশ করে।

এই একটা bug — যেটা প্রতিটা C/C++ প্রোগ্রামার জীবনে অন্তত একবার লেখে — পুরোপুরি বোঝা যায় যদি আপনি জানেন unsigned integer আসলে কী। এই লেসনের উদ্দেশ্য ঠিক সেটাই।

আগের লেসনে (bits, bytes, words) আপনি দেখেছেন কীভাবে একটা bit pattern-কে binary, hex, decimal-এ পড়া যায় — positional notation। এই লেসন সেই গল্পটা আর repeat করবে না। এখানে প্রশ্ন আলাদা:

  • সেই pattern-এর range ঠিক কতদূর?
  • সেই range-এর বাইরে গেলে ঠিক কী ঘটে — এলোমেলো কিছু, নাকি একটা সুনির্দিষ্ট নিয়ম?
  • আর কেন সেই নিয়মটাই বাস্তব bug-এর সবচেয়ে বড় উৎসগুলোর একটা?

Level 0-তে number-systems লেসনে আমরা একটা ঘড়ির উদাহরণ দিয়ে শুরু করেছিলাম — ৯টায় ৫ ঘণ্টা যোগ করলে ১৪ নয়, । Unsigned integer আক্ষরিক অর্থে সেই ঘড়ি। শুধু ঘড়িতে ১২টা ঘর, এখানে 2ⁿ টা।

মূল ধারণা

দ্রুত recap — n bit থেকে মান

আগের লেসনে দেখেছেন: n bit-এর একটা pattern dₙ₋₁ … d₁ d₀ (প্রতিটা dᵢ ∈ {0, 1}) unsigned interpretation-এ মানে:

value=i=0n1di2i\text{value} = \sum_{i=0}^{n-1} d_i \cdot 2^i

এটা ঠিক দশমিকের মতোই positional notation, শুধু base 10-এর বদলে base 2। এই লেসন এই সূত্রটা আবার derive করবে না — এটা ধরে নিচ্ছি আপনি জানেন। আমাদের আগ্রহ এখন দুইটা নতুন প্রশ্নে: range আর arithmetic

Range — কতদূর যেতে পারে

n bit-এ মোট 2ⁿ টা আলাদা pattern সম্ভব (প্রতিটা bit স্বাধীনভাবে 0 বা 1)। Unsigned interpretation-এ এই 2ⁿ টা pattern সবচেয়ে ছোট থেকে সবচেয়ে বড় পর্যন্ত ফাঁকহীনভাবে সাজানো — 0 থেকে 2ⁿ − 1

প্রস্থC টাইপমোট patternRange
8 bituint8_t2560 থেকে 255
16 bituint16_t65,5360 থেকে 65,535
32 bituint32_t4,294,967,296 (~৪.৩ বিলিয়ন)0 থেকে 4,294,967,295
64 bituint64_t~1.8 × 10¹⁹0 থেকে 18,446,744,073,709,551,615

কেন 2ⁿ − 1, 2ⁿ নয়: সব bit 1 হলে মান 2^(n-1) + 2^(n-2) + \cdots + 2^0 = 2ⁿ − 1। এটা geometric series-এর যোগফল — Level 0-এর কম্বিনেটরিক্স লেসনে এই যোগফলের প্রমাণ দেখেছেন। মনে রাখার সহজ কৌশল: 2ⁿ মানে 1 তারপর n টা শূন্য (10000...0₂), তার থেকে 1 বিয়োগ করলে ঠিক n টা 1 (0111...1₂)।

C-তে unsigned টাইপগুলো

#include <stdint.h>   // exact-width টাইপের জন্য

uint8_t  a = 200;      // 0 .. 255,                 ঠিক ৮ bit
uint16_t b = 40000;     // 0 .. 65,535,              ঠিক ১৬ bit
uint32_t c = 3000000000u; // 0 .. 4,294,967,295,      ঠিক ৩২ bit
uint64_t d = 10000000000000000000ull; // 0 .. ~1.8×10¹⁹, ঠিক ৬৪ bit

stdint.h-এর uintN_t টাইপগুলো exact-width — গ্যারান্টি দেয় ঠিক N bit, কোনো platform-এ কম-বেশি নয়। এর আগে (C99-এর আগে) unsigned int, unsigned long ইত্যাদির প্রস্থ platform-ভেদে বদলাত — একটা unsigned long কোথাও 32-bit, কোথাও 64-bit। এই অনিশ্চয়তাই stdint.h তৈরির মূল কারণ।

Unsigned addition = arithmetic mod 2ⁿ

এবার আসল বিষয়ে। ধরুন uint8_t-এ 250 + 10 করছেন। গাণিতিকভাবে উত্তর 260। কিন্তু uint8_t মাত্র 256 টা মান ধরে রাখতে পারে (0255)। 260 তার মধ্যে নেই। তাহলে কী হবে?

uint8_t a = 250, b = 10;
uint8_t c = a + b;
printf("%u\n", c);   // ছাপবে: 4

260 নয়, 4। কেন? কারণ:

260mod256=4260 \bmod 256 = 4

এটা কাকতালীয় নয় — এটা সংজ্ঞা। Number-systems লেসনে আমরা প্রমাণ করেছিলাম:

একটা n-bit unsigned integer আসলে ℤ_(2ⁿ)-এর একটা সদস্য। CPU-র যোগ, বিয়োগ ও গুণ আক্ষরিকভাবে mod 2ⁿ operation।

এখানে সেই লাইনটাই ফিরে আসছে, শুধু এবার পুরোপুরি unsigned-এর প্রসঙ্গে, কোনো signed জটিলতা ছাড়া। C standard (ISO/IEC 9899:2018, §6.2.5 ¶9) আক্ষরিকভাবে বলে:

“A computation involving unsigned operands can never overflow, because a result that cannot be represented by the resulting unsigned integer type is reduced modulo the number that is one greater than the largest value that can be represented.”

মানে: uint8_t-এর যোগফল সবসময় mod 256 reduce হয়। এটা কোনো accident বা hardware limitation না ধরা পড়া bug — এটা ভাষার নিয়ম

বিট দিয়ে দেখা যাক — carry chain

250 + 10 কে বাইনারিতে যোগ করুন, হাতে-কলমে ripple carry দিয়ে:

  250 = 1111 1010
+  10 = 0000 1010
-------------------
        1 0000 0100    ← ৯ bit ফলাফল!

কলাম ধরে ধরে (ডান থেকে বাঁয়ে, প্রতিটায় carry-in যোগ):

bit position76543210
a11111010
b00001010
carry-in11100000
sum bit00010000
carry-out11110000

সবচেয়ে বাঁয়ের (bit 7) carry-out 1 — এই 1 টাই ৯ম bit হয়ে “উপচে” পড়ছে। কিন্তু uint8_t মাত্র ৮টা bit ধরে রাখতে পারে। সেই ৯ম bit-টা রাখার কোনো জায়গা নেই — CPU সেটা register-এর বাইরে ফেলে দেয় (একটা flag-এ সংরক্ষণ করে, নিচে দেখব)। যা টিকে থাকে সেটা নিচের ৮টা bit: 0000 0100 = 4। ঠিক 260 mod 256

   পূর্ণ গাণিতিক যোগফল:   1 0000 0100   (৯ bit, = 260)
                          ↓  ফেলে দেওয়া হলো
   uint8_t-এ যা থাকে:      0000 0100   (৮ bit, = 4)
৯ম bit উপচে পড়ল, ৮টা bit-এর 'ঘর'-এ শুধু নিচের ৮টাই টিকল।

এটাই ঘড়ির গণিত হাতে-নাতে। ৯টা বাজে ৫ ঘণ্টা যোগ করলে ঘড়ি “১৪” দেখায় না — কারণ ঘড়িতে “১৪”-এর জন্য কোনো ঘর নেই, শুধু ১২টা ঘর (mod 12, সাধারণত 1-12 লেখা থাকে বলে সামান্য shift আছে)। uint8_t-তে 256 টা ঘর — 0 থেকে 255

Carry-out বনাম overflow — unsigned-এ এরা একই জিনিস

এখানে একটা পরিভাষাগত পার্থক্য স্পষ্ট করা দরকার, কারণ পরের লেসনে (signed integers) এই দুটো আলাদা হয়ে যাবে।

  • Carry-out: সবচেয়ে গুরুত্বপূর্ণ bit (MSB) যোগ করার সময় যদি একটা অতিরিক্ত 1 “উপচে” পড়ে, সেটাই carry-out। এটা CPU-র একটা hardware সংকেত — x86-এ এটা Carry Flag (CF)-এ সংরক্ষিত হয়।
  • Unsigned overflow: গাণিতিক ফলাফল representable range-এর বাইরে গেছে কি না।

Unsigned addition-এ এই দুইটা ঠিক একই ঘটনা। যদি a + b representable range-এর (02ⁿ−1) বাইরে যায়, সেটা ঘটে কেবল তখনই যখন MSB-তে carry-out হয়। প্রমাণ সহজ: a, b \< 2ⁿ, তাই a + b \< 2 \times 2ⁿ = 2^(n+1)। ফলাফল 2ⁿ বা তার বেশি হলেই তা n-bit ঘরে আঁটবে না, আর সেটা মানেই ঠিক n-তম bit-এ (bit index n, অর্থাৎ MSB-এর পরের bit) একটা 1 — যেটাই carry-out।

তাই unsigned-এ:

overflow ঘটেছে    carry-out=1\text{overflow ঘটেছে} \iff \text{carry-out} = 1

uint32_t a = 4000000000u, b = 1000000000u;
uint32_t sum = a + b;          // wraps
int overflowed = (sum \< a);    // যদি ফলাফল a-এর চেয়ে ছোট হয়ে যায়, wrap হয়েছে
printf("sum=%u overflow=%d\n", sum, overflowed);
// sum=705032704 overflow=1

sum \< a চেকটা কাজ করে কারণ unsigned addition monotonic — b \geq 0 হলে a + b \geq a হওয়ার কথা। এটা না হলে বোঝা যায় wraparound ঘটেছে। (সতর্কতা: পরের লেসনে দেখবেন এই একই ধরনের চেক signed-এ কাজ করে না — এটা এই লেসনের সবচেয়ে গুরুত্বপূর্ণ সতর্কবার্তা, যেটার পুরো ব্যাখ্যা লেসন ৬-এ।)

Unsigned subtraction “wrap” করে — এটা bug নয়, সংজ্ঞা

এবার সবচেয়ে গুরুত্বপূর্ণ প্রশ্ন: uint8_t-এ 5 - 8 কী?

uint8_t a = 5, b = 8;
uint8_t c = a - b;
printf("%u\n", c);   // ছাপবে: 253

253? গাণিতিকভাবে 5 - 8 = -3, একটা ঋণাত্মক সংখ্যা। কিন্তু uint8_t-এ ঋণাত্মক সংখ্যা বলে কিছু নেই — সব মান 0 থেকে 255। তাহলে -3 কোথায় গেল?

3253(mod256)-3 \equiv 253 \pmod{256}

কারণ 253 = 256 - 3। ঠিক number-systems লেসনের congruence-এর সংজ্ঞা: a ≡ b (mod m) ⟺ m | (a - b)। এখানে -3 আর 253 একই equivalence class [253]-এর সদস্য mod 256-এ।

এটা কোনো ত্রুটি না, কোনো “undefined” আচরণ না। C standard (§6.2.5 ¶9, উপরে উদ্ধৃত) এটাকে সরাসরি সংজ্ঞায়িত করে: unsigned বিয়োগের ফলাফল যদি representable range-এর বাইরে যায় (অর্থাৎ ঋণাত্মক হয়), সেটা mod 2ⁿ reduce হয়ে ফিরে আসে — সবসময়, প্রতিটা compiler-এ, প্রতিটা platform-এ, একইভাবে।

হাতে-কলমে — বিয়োগও আসলে যোগ

CPU-তে বিয়োগের জন্য আলাদা circuit লাগে না। a - b গণনা করা হয় a + (2ⁿ - b) হিসেবে — অর্থাৎ b-এর additive inverse mod 2ⁿ যোগ করে।

uint8_t-এ:  5 - 8
          = 5 + (256 - 8)
          = 5 + 248
          = 253            ✓ (মড়ুলো ৮ কেটে গেলে যা থাকে)

256 - 8 = 248-কে বাইনারিতে বানানোর কৌশলটাই — bit উল্টে 1 যোগ — পরের লেসনের (signed integers, two’s complement) কেন্দ্রীয় বিষয়। এখানে শুধু এইটুকু মনে রাখুন: unsigned world-এও বিয়োগ আসলে গোপনে একটা যোগ, আর সেই গোপন কৌশলটাই দুই লেসন পরে সম্পূর্ণ প্রকাশ পাবে।

size_t — কেন unsigned, আর কোথায় কামড়ায়

size_t হলো C/C++-এর একটা built-in typedef, যেটা “কোনো object-এর আকার ধরে রাখার জন্য যথেষ্ট বড় unsigned integer টাইপ” হিসেবে সংজ্ঞায়িত (C standard, stddef.h)। sizeof operator-এর ফলাফলের টাইপ এটাই, আর malloc(), strlen(), std::vector::size() — সবাই size_t ফেরত দেয়।

Platformsize_t-এর প্রস্থ
32-bit (x86, ARM32)সাধারণত 32 bit — uint32_t-এর মতো
64-bit (x86-64, ARM64)সাধারণত 64 bit — uint64_t-এর মতো

কেন unsigned: একটা array বা memory block-এর আকার কখনো ঋণাত্মক হতে পারে না — -5 byte-এর কোনো অর্থ নেই। তাই টাইপ-সিস্টেমে সেই সত্যটা এনকোড করে দেওয়া স্বাভাবিক মনে হয়েছিল ডিজাইনারদের কাছে: “যদি এটা ঋণাত্মক হতেই না পারে, unsigned দাও, তাহলে অর্ধেক representable range নষ্ট হবে না, আর ভুল করে ঋণাত্মক মান পাঠালে কম্পাইলার সতর্ক করবে।”

সমস্যাটা হলো এই দ্বিতীয় আশাটা বাস্তবে উল্টো ফল দেয়। size_t ঋণাত্মক হতে পারে না বলে representation-এ কোনো ভুল ঋণাত্মক মান “clamp” হয় না — বরং wrap করে বিশাল ধনাত্মক সংখ্যায় পরিণত হয়, যেটা প্রায়ই আরও বিপজ্জনক, কারণ সেটা দেখতে একটা বৈধ, বড় array-index-এর মতো।

সেই for loop-টা আবার, এবার পুরোপুরি বিশ্লেষণ করে

std::vector<int> v = {10, 20, 30};
for (size_t i = v.size() - 1; i >= 0; i--) {
    std::cout << v[i] << " ";
}

দুইটা আলাদা সমস্যা এখানে জড়িয়ে আছে — দুটোকেই আলাদা করে দেখা দরকার, কারণ মানুষ প্রায়ই শুধু একটা ধরে অন্যটা মিস করে।

সমস্যা ১ — i >= 0 শর্তটা কখনোই মিথ্যা হয় না।

size_t unsigned। Unsigned-এর সবচেয়ে ছোট মান 0। তাই i >= 0 সবসময় সত্য, i-এর মান যাই হোক — এটা tautology। Compiler এটা জানে, আর -Wall -Wextra দিলে ঠিক এই কথাই সতর্ক করে:

warning: comparison of unsigned expression '>= 0' is always true

তাই loop-টা আসলে i-- করতে করতে যখন i == 0 হয়, তার পরের বার i-- করলে i হয় SIZE_MAX (৬৪-bit-এ 18,446,744,073,709,551,615) — এখনো >= 0। Loop থামে না। v[i]-তে astronomically বড় একটা index দিয়ে access — undefined behavior, সাধারণত crash।

সমস্যা ২ — খালি vector হলে প্রথম iteration-এই বিপর্যয়।

v যদি খালি থাকে, v.size() হয় 0v.size() - 1 তখন 0 - 1 — unsigned world-এ, যেটা আমরা এইমাত্র শিখলাম, mod 2⁶⁴ হিসেবে wrap করে SIZE_MAX। Loop প্রথম iteration-এই v[SIZE_MAX]-এ হাত দেয় — কোনো warning ছাড়াই, কারণ syntax-এর দিক থেকে সবকিছু বৈধ।

v.size()সমস্যাফলাফল
0size() - 1 তাৎক্ষণিক wrapপ্রথম access থেকেই OOB
> 0i >= 0 কখনো মিথ্যা হয় নাশেষ পর্যন্ত পৌঁছে wrap, তারপর OOB

সঠিক লেখা — তিনটা বিকল্প:

// বিকল্প ১: signed loop variable, size()-কে cast করুন
for (int i = (int)v.size() - 1; i >= 0; i--) { ... }

// বিকল্প ২: postfix i-- trick — i শূন্যে পৌঁছানোর *আগেই* থামে
for (size_t i = v.size(); i-- > 0; ) {
    std::cout << v[i] << " ";
}

// বিকল্প ৩ (C++20+): reverse iterator, index-ই লাগে না
for (auto it = v.rbegin(); it != v.rend(); ++it) {
    std::cout << *it << " ";
}

দ্বিতীয় বিকল্পটা কেন কাজ করে, একটু থেমে বুঝে নেওয়া দরকার: i-- > 0 এক্সপ্রেশনে প্রথমে তুলনা হয় পুরনো i দিয়ে, তারপর i কমে। তাই i = v.size() (যেমন 3) দিয়ে শুরু হয়ে, লুপের ভেতরে ব্যবহৃত i-এর মান হয় 2, 1, 0 — আর i যখন 0-এ পৌঁছে 0 > 0 মিথ্যা হয়ে যায়, তখনই থামে, তার আগেই — কখনো wrap হওয়ার সুযোগ পায় না। v.size() == 0 হলেও নিরাপদ: i = 0, 0 > 0 মিথ্যা, loop body একবারও চলে না।

Integer promotion — ছোট unsigned টাইপের একটা চমক

শেষ একটা সূক্ষ্ম কিন্তু গুরুত্বপূর্ণ ব্যাপার। uint8_t বা uint16_t-এর মতো ছোট unsigned টাইপে arithmetic করলে C একটা আশ্চর্যজনক নিয়ম প্রয়োগ করে — integer promotion

uint8_t a = 200, b = 100;

// আপনি ভাবতে পারেন a + b একটা uint8_t, তাই wrap করবে (300 mod 256 = 44)
if (a + b > 255) {
    printf("overflow!\n");   // এটা কি ছাপা হবে?
}

হ্যাঁ, "overflow!" ছাপা হবে — কিন্তু কারণটা আপনার ধারণার উল্টো। C-র নিয়ম বলে: int-এর চেয়ে ছোট যেকোনো integer টাইপ, arithmetic operation-এ অংশ নেওয়ার আগে, int-এ promote হয় (যদি int তার সব মান ধারণ করতে পারে, যা uint8_t-এর ক্ষেত্রে সবসময় সত্য, কারণ int কমপক্ষে ১৬ bit, বাস্তবে প্রায় সবসময় ৩২)।

তাই a + b আসলে (int)a + (int)b = 200 + 100 = 300 হিসেবে গণনা হয় — কোনো wraparound হয় না, কারণ 300 সহজেই int-এ আঁটে। তুলনাটা 300 > 255 — সত্য।

কিন্তু:

uint8_t c = a + b;
printf("%u\n", c);   // ছাপবে: 44

এখানে int-এ গণনা করা 300-কে যখন uint8_t ভেরিয়েবলে assign করা হয়, তখনই mod 256 reduction ঘটে — 300 mod 256 = 44

Expressionটাইপমান
a + b (নিজে থেকে)int (promoted)300
a + b > 255int তুলনাসত্য
uint8_t c = a + b;assign-এ truncate44

শিক্ষা: wraparound ঘটে assignment বা explicit cast-এর সময়, expression evaluation-এর মাঝপথে নয় — যদি না পুরো expression-ই int-এর চেয়ে বড় বা সমান প্রস্থের unsigned টাইপে হয় (uint32_t, uint64_t — এদের int-এ promote করা হয় না, বরং উল্টোটা, int এদের টাইপে convert হয়)। uint32_t/uint64_t নিয়ে কাজ করলে promotion-এর এই চমক নেই — সরাসরি সেই টাইপেই wrap করে, যেমনটা এই লেসনের বাকি সব উদাহরণে দেখিয়েছি।

একটা 'unsigned wraparound' সিস্টেমের কোথায় কোথায় দেখা যায়
  1. a + b mod 2ⁿভাষার সংজ্ঞা — সবসময় well-defined
  2. ADD/SUB instructionx86: ফলাফল truncate, CF-এ carry/borrow
  3. ripple-carry adder circuitMSB-এর carry-out simply discarded — Level 2-এ
  4. size_t loop bound`v.size() - 1`-এর wrap — এই লেসনের মূল bug
  5. hash table bucket count`capacity - 1` mask — capacity=0 হলে বিপর্যয়
  6. network protocol length fieldকম দৈর্ঘ্য ঘোষণা + negative offset = heap overflow

ভেতরে কী ঘটছে

Carry Flag — hardware কীভাবে জানে wrap হয়েছে

CPU-র ALU (Arithmetic Logic Unit) যখন দুইটা n-bit সংখ্যা যোগ করে, তখন আসলে n+1-bit precision-এ গণনা করে — শেষ bit-টা সংরক্ষণ করা হয় একটা আলাদা ১-bit hardware register-এ, x86-এ যার নাম Carry Flag (CF), FLAGS/EFLAGS/RFLAGS register-এর একটা bit।

ADD instruction চালানোর পর:
  destination register  ← নিচের n bit (truncated ফলাফল)
  CF                     ← MSB-এর carry-out (৯ম/১৭তম/৩৩তম/৬৫তম bit)

এই flag-টাই পরে branch instruction পড়ে:

; C: if (a + b < a) overflow = 1;
mov  eax, [a]
add  eax, [b]         ; eax = a+b (truncated), CF সেট হয় যদি wrap
jc   overflow_handler  ; JC = Jump if Carry — সরাসরি CF পড়ে

jc (jump if carry) instruction-টা লক্ষ্য করুন — এটা কোনো তুলনা করছে না, শুধু আগের add-এর সময় set হওয়া CF bit-টা পড়ছে। এই কারণেই sum \< a স্টাইলের C কোড কম্পাইল হলে প্রায়ই সরাসরি jc-তে পরিণত হয় — compiler বুঝে ফেলে আপনি আসলে CF-ই জানতে চাইছেন।

যেখানে এই “নিরাপদ, সুসংজ্ঞায়িত” abstraction বিপজ্জনক হয়ে ওঠে

Unsigned wraparound সুসংজ্ঞায়িত মানে এই না যে এটা নিরাপদ। বরং উল্টো — এটা এত predictable বলেই attacker এটা কাজে লাগাতে পারে।

Signedness leak — implicit conversion-এর ফাঁদ

C-তে signed আর unsigned মেশালে একটা lesser-known নিয়ম কাজ করে: “usual arithmetic conversions”। একটা int আর একটা unsigned int তুলনা করলে, int-টা চুপচাপ unsigned-এ convert হয়ে যায় — value বদলে যেতে পারে, কিন্তু কোনো warning ছাড়াই (-Wsign-compare দিলে অবশ্য warning আসে)।

int x = -1;
unsigned int y = 5;

if (x > y) {
    printf("x > y!\n");   // এটাই ছাপা হবে!
}

-1 > 5 সত্য?! হ্যাঁ — কারণ তুলনার আগে x (-1) unsigned-এ convert হয়। int-এ -1-এর bit pattern হলো সব bit 1 (দেখবেন কেন, লেসন ৫-এ)। সেই একই bit pattern unsigned হিসেবে পড়লে UINT_MAX — ৩২-bit-এ 4,294,967,295। আর 4,294,967,295 > 5 অবশ্যই সত্য।

int x = -1        →  bit pattern: 1111...1111 (32 bit)
unsigned convert   →  same bits read as unsigned: 4,294,967,295

এই একই bit pattern, দুইটা সম্পূর্ণ আলাদা মান — সব নির্ভর করে কে জিজ্ঞেস করছে। এটাই representation বনাম interpretation-এর কেন্দ্রীয় থিম যেটা এই মডিউল বারবার ফিরে আসবে।

বাস্তব bug প্যাটার্ন:

int read_data(char *buf, int len) {
    if (len > MAX_SIZE) return -1;      // "নিরাপত্তা চেক" — কিন্তু...
    memcpy(dest, buf, len);              // memcpy-এর তৃতীয় parameter: size_t!
}

len যদি কোনোভাবে ঋণাত্মক হয় (উদাহরণ: একটা ভাঙা network packet থেকে পড়া), len > MAX_SIZE চেকটা signed তুলনা হিসেবে মিথ্যা হয়ে যাবে (ঋণাত্মক সংখ্যা তো MAX_SIZE-এর চেয়ে ছোট)। কিন্তু memcpy(dest, buf, len)-এ len কে size_t-তে implicit convert করা হয় — একটা বিশাল ধনাত্মক সংখ্যা, আর memcpy সেই বিশাল সংখ্যক byte কপি করার চেষ্টা করে — classic buffer overflow। এই একই প্যাটার্ন দশকের পর দশক ধরে বাস্তব CVE-তে দেখা গেছে, বিশেষ করে image/video parsing library-তে যেখানে length field network থেকে আসে।

“unsigned টাইপ ব্যবহার করলে ঋণাত্মক-সংখ্যা bug থেকে নিরাপদ থাকা যায়।”

এটা একটা দীর্ঘদিনের বিতর্ক, আর সরল উত্তর নেই। যুক্তিটা দুই দিক থেকেই আসে:

Unsigned-পন্থীদের যুক্তি: একটা array-এর length ঋণাত্মক হতে পারে না, তাই টাইপে সেটা এনকোড করা উচিত। C++ standard library (std::vector::size(), std::string::length()) এই দর্শন অনুসরণ করে।

Signed-পন্থীদের যুক্তি (Google C++ Style Guide বহু বছর এটাই বলেছে, আর Bjarne Stroustrup নিজেও পরবর্তীতে একমত হন): unsigned-এর “সুরক্ষা” বাস্তবে বিপরীত ফল দেয় — ভুল করে ঋণাত্মক হিসাব হলে (যেমন size() - 1 খালি container-এ) সেটা clamp হয়ে 0 হওয়ার বদলে বিশাল ধনাত্মক সংখ্যায় wrap করে, যা crash-এর বদলে silent memory corruption ঘটাতে পারে। আর signed/unsigned মেশানোর প্রতিটা তুলনায় (উপরের x > y উদাহরণ) একটা লুকানো ফাঁদ থাকে।

বাস্তবতা: কোনো পক্ষই সম্পূর্ণ ঠিক না — এটাই কারণ এই লেসনের পুরোটা জুড়ে “wraparound সংজ্ঞায়িত মানে নিরাপদ নয়” কথাটা বারবার এসেছে। আধুনিক পরামর্শ (C++ Core Guidelines ES.100–ES.107): signed আর unsigned কখনো একসাথে তুলনা করবেন না; index-এর জন্য ssize_t/signed টাইপ বিবেচনা করুন যেখানে সম্ভব; unsigned শুধু bit-manipulation-এর জন্য রাখুন যেখানে wraparound-ই কাম্য আচরণ (hash, checksum, bitmask)।

উদাহরণ

একটা সম্পূর্ণ উদাহরণ — uint16_t কাউন্টার

ধরুন আপনি একটা সেন্সর থেকে packet counter পড়ছেন, uint16_t-এ রাখা (range 065535)। Counter 65,533 থেকে শুরু করে প্রতি packet-এ 1 করে বাড়ছে। ৫টা packet আসার পর মান কী?

65533+5=6553865533 + 5 = 65538

কিন্তু uint16_t-এর range মাত্র 65,536 টা মান (065,535)।

65538mod65536=265538 \bmod 65536 = 2

বিট দিয়ে যাচাই:

  65533 = 1111 1111 1111 1101
+     5 = 0000 0000 0000 0101
------------------------------
   1 0000 0000 0000 0010    ← ১৭ bit ফলাফল

উপচে পড়া ১৭তম bit ফেলে দিলে থাকে 0000 0000 0000 0010 = 2

Packet #Counter আগে+1Counter পরে
6553365534
6553465535 (max!)
655350 ← wrap!
01
12

এই টেবিলটা লক্ষ্য করুন — এটাই আসলে “চাকা ঘোরা”। 65535-এর পরের ধাপ 0, ঠিক ঘড়ির কাঁটা 12-এর পরে 1-এ ফিরে যাওয়ার মতো (শুধু এখানে ফিরে যাওয়ার bin 0, কারণ কম্পিউটারের গণনা 0 থেকে শুরু হয়)।

এটা কি একটা bug? সম্পূর্ণ নির্ভর করে উদ্দেশ্যের উপর।

  • packet counter যদি শুধু “কত packet গেছে” track করার জন্য হয়, wraparound-এ একটা ambiguity তৈরি হয় — protocol design-এর সময় এটা মাথায় রাখতে হবে (যেমন TCP sequence number, যা আমরা number-systems লেসনে PAWS mechanism দিয়ে দেখেছি)।
  • counter যদি একটা ring buffer index হয় (idx % capacity বা idx & (capacity - 1)), তাহলে wraparound-ই ঠিক কাম্য আচরণ — এটাই ring buffer-এর সংজ্ঞা।

মূল শিক্ষা: unsigned wraparound নিজে কখনো “ভুল” না — ভুলটা হয় যখন ডিজাইনার এই আচরণ ধরে নেননি, বা যেখানে wrap হওয়া উচিত না সেখানে হয়ে যায়।

নিজে চালিয়ে দেখুন

EXPERIMENT

uint8_t wraparound হাতে-নাতে দেখুন — আর কম্পাইলার-ওয়ার্নিং যাচাই করুন

Linux / macOS / WSL (gcc বা clang)· ১৫ মিনিট

ধাপ ১ — একটা ছোট wraparound demo।

// wrap.c
#include <stdio.h>
#include <stdint.h>

int main(void) {
    uint8_t x = 250;
    for (int i = 0; i \< 10; i++) {
        printf("x = %3u  (0x%02x)\n", x, x);
        x = x + 1;
    }
    return 0;
}
gcc -O2 -Wall -Wextra -o wrap wrap.c
./wrap

প্রত্যাশিত output:

x = 250  (0xfa)
x = 251  (0xfb)
x = 252  (0xfc)
x = 253  (0xfd)
x = 254  (0xfe)
x = 255  (0xff)
x =   0  (0x00)    ← ঠিক এখানে wrap
x =   1  (0x01)
x =   2  (0x02)
x =   3  (0x03)

255 -এর পরে 256 নয়, 0। কোনো error, কোনো crash, কোনো warning — কারণ এই ভাষার সংজ্ঞা অনুযায়ীই এটা সঠিক আচরণ।

ধাপ ২ — size_t reverse-loop bug লাইভ দেখুন (নিয়ন্ত্রিতভাবে)।

সরাসরি অসীম লুপ চালানো বিপজ্জনক (crash করতে সময় লাগতে পারে)। তার বদলে প্রথম কয়েকটা iteration দেখে বুঝে নিন, একটা কাউন্টার দিয়ে নিরাপদে থামিয়ে:

// sizet_bug.c
#include <stdio.h>
#include <stddef.h>

int main(void) {
    size_t n = 0;                 // ফাঁকা "array"-এর আকার
    size_t i = n - 1;             // ভুল প্যাটার্ন — n==0 হলে বিপর্যয়
    printf("n - 1 হিসেবে পাওয়া গেল: %zu\n", i);

    int guard = 0;
    for (size_t j = 3 - 1; j >= 0 && guard \< 8; j--, guard++) {
        printf("  j = %zu  (guard = %d)\n", j, guard);
        if (j == 0) {
            printf("  j-- করার পর wrap করবে -> ");
        }
    }
    return 0;
}
gcc -O2 -Wall -Wextra -o sizet_bug sizet_bug.c

কম্পাইল করার সময়ই দেখুন কী warning আসে:

sizet_bug.c:12:29: warning: comparison of unsigned expression
  in 'j >= 0' is always true [-Wtype-limits]

Compiler নিজেই বলে দিচ্ছে এই তুলনাটা অর্থহীন — কারণ size_t কখনো 0-এর কম হতেই পারে না। চালান:

./sizet_bug
n - 1 হিসেবে পাওয়া গেল: 18446744073709551615
  j = 2  (guard = 0)
  j = 1  (guard = 1)
  j = 0  (guard = 2)
  j-- করার পর wrap করবে ->   j = 18446744073709551615  (guard = 3)
  j = 18446744073709551614  (guard = 4)
  j = 18446744073709551613  (guard = 5)
  j = 18446744073709551612  (guard = 6)
  j = 18446744073709551611  (guard = 7)

n - 1 (যখন n = 0) সরাসরি SIZE_MAX-এ পরিণত হলো, আর loop j = 0 পেরিয়ে থামার বদলে ঠিক SIZE_MAX-এ লাফ দিল — guard না থাকলে এই loop trillion trillion বার চলত, প্রতিবার আরও “নিচে” নামত, বাস্তবে যদি এটা v[j] access করত তাহলে সেকেন্ডের ভগ্নাংশে crash করত।

ধাপ ৩ — সঠিক তিনটা প্যাটার্ন যাচাই করুন।

#include <stdio.h>
#include <stddef.h>

int main(void) {
    int arr[] = {10, 20, 30};
    size_t n = sizeof(arr) / sizeof(arr[0]);

    printf("postfix trick: ");
    for (size_t i = n; i-- > 0; ) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    // খালি array-তেও নিরাপদ কি না যাচাই করুন
    size_t m = 0;
    int safe = 1;
    for (size_t i = m; i-- > 0; ) {
        safe = 0;
    }
    printf("খালি array-তে body চলেনি: %s\n", safe ? "হ্যাঁ" : "না");
    return 0;
}

প্রত্যাশিত:

postfix trick: 30 20 10
খালি array-তে body চলেনি: হ্যাঁ
এটা কী প্রমাণ করে

unsigned wraparound সত্যিই সংজ্ঞায়িত ও predictable — কোনো এলোমেলো crash নয়, precise mod 2ⁿ arithmetic। আর কম্পাইলারের -Wextra ফ্ল্যাগ ঠিক এই bug class-টা ধরতে পারে।

EXPERIMENT

Python-এ fixed-width unsigned জোর করে সিমুলেট করুন

Python 3 (numpy বা ctypes)· ১০ মিনিট
# প্রথমে দেখুন Python নিজে কী করে
x = 250
for _ in range(10):
    print(x)
    x += 1
# 250, 251, ..., 259 — কখনোই wrap করে না, Python int সীমাহীন precision-এর
import numpy as np

x = np.uint8(250)
for _ in range(10):
    print(int(x))
    x = np.uint8(x + 1)   # numpy আসল fixed-width arithmetic করে
# 250, 251, ..., 255, 0, 1, 2, 3  — এবার C-র মতোই wrap করে
import ctypes

x = ctypes.c_uint8(5)
y = ctypes.c_uint8(8)
result = ctypes.c_uint8(x.value - y.value)
print(result.value)   # 253 — ঠিক C-র মতো

পার্থক্যটা কেন গুরুত্বপূর্ণ: Python-এর int কোনো fixed bit-width রাখে না — তাই “overflow” ধারণাটাই Python-এর pure int-এ প্রযোজ্য নয়। wraparound একটা choice, যেটা C/C++/Rust/Java তাদের fixed-width টাইপের জন্য নেয় (কারণ hardware register-ও fixed-width), কিন্তু Python ইচ্ছাকৃতভাবে এড়িয়ে গেছে (বিনিময়ে প্রতিটা বড় সংখ্যার arithmetic ধীর — বড় integer memory-তে dynamically allocate হওয়া array হিসেবে থাকে)।

এটা কী প্রমাণ করে

Python-এর built-in int arbitrary precision — কখনো overflow করে না। Fixed-width wraparound দেখতে হলে ইচ্ছাকৃতভাবে C-স্টাইল টাইপ চাপাতে হয়, যা দেখিয়ে দেয় wraparound হার্ডওয়্যার/ভাষার সিদ্ধান্ত, গণিতের বাধ্যবাধকতা নয়।

নিজে বানান

BUILD IT

Overflow-Aware Unsigned Calculator

C · ●●○○○
  1. uint8_t, uint16_t, uint32_t, uint64_t — প্রতিটার জন্য একটা checked_add ফাংশন লিখুন যা ফলাফল আর overflow হয়েছে কি না দুটোই ফেরত দেয়
  2. একটা print_binary ফাংশন লিখুন যা যেকোনো unsigned মানের সব bit ছাপায়, MSB আগে
  3. checked_add-এর ফলাফল আর carry-out টেবিল আকারে পাশাপাশি ছাপান, ঠিক এই লেসনের হাতে-করা টেবিলের মতো
  4. একটা safe_last_index(size_t n) ফাংশন লিখুন যা n==0 হলে একটা "নেই" সংকেত (bool ফেরত মান) দেয়, নাহলে n-1 দেয় — আর সেটা দিয়ে একটা সঠিক reverse-loop চালান
  5. ইচ্ছেমতো uint32_t দুটো র‍্যান্ডম মান নিয়ে ১০,০০০ বার যোগ করে দেখুন কত শতাংশ ক্ষেত্রে overflow হয় — এবং সেটা তাত্ত্বিক সম্ভাবনার সাথে মেলান
/*
 * unsigned_lab.c — overflow-aware unsigned arithmetic টুলকিট
 * চালান:  gcc -O2 -Wall -Wextra -o unsigned_lab unsigned_lab.c && ./unsigned_lab
 */
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <time.h>

/* ── ১. Checked addition — প্রতিটা প্রস্থের জন্য ─────────────────── */

static bool checked_add_u8(uint8_t a, uint8_t b, uint8_t *out) {
    *out = (uint8_t)(a + b);
    return *out \< a;              /* wrap হলে ফলাফল a-এর চেয়ে ছোট হবে */
}

static bool checked_add_u32(uint32_t a, uint32_t b, uint32_t *out) {
    *out = a + b;
    return *out \< a;
}

static bool checked_add_u64(uint64_t a, uint64_t b, uint64_t *out) {
    *out = a + b;
    return *out \< a;
}

/* ── ২. Binary print ──────────────────────────────────────────── */

static void print_binary(uint64_t value, int bits) {
    for (int i = bits - 1; i >= 0; i--) {
        putchar((value >> i) & 1 ? '1' : '0');
        if (i % 4 == 0 && i != 0) putchar(' ');
    }
}

/* ── ৩. একটা টেবিল বানানো ─────────────────────────────────────── */

static void demo_table(void) {
    struct { uint8_t a, b; } cases[] = {
        {250, 10}, {5, 8}, {200, 100}, {255, 1}, {0, 0}, {128, 128},
    };
    printf("  %5s %5s   %-9s %-9s  overflow\n", "a", "b", "a bits", "result");
    for (size_t i = 0; i \< sizeof(cases)/sizeof(cases[0]); i++) {
        uint8_t out;
        bool ovf = checked_add_u8(cases[i].a, cases[i].b, &out);
        printf("  %5u %5u   ", cases[i].a, cases[i].b);
        print_binary(cases[i].a, 8);
        printf("  = %-3u  ", out);
        printf("%s\n", ovf ? "হ্যাঁ" : "না");
    }
}

/* ── ৪. নিরাপদ last-index ─────────────────────────────────────── */

static bool safe_last_index(size_t n, size_t *out) {
    if (n == 0) return false;      /* খালি — কোনো বৈধ index নেই */
    *out = n - 1;                  /* n > 0, তাই এই বিয়োগ নিরাপদ */
    return true;
}

static void demo_safe_loop(void) {
    int arr[] = {10, 20, 30};
    size_t n = sizeof(arr) / sizeof(arr[0]);

    size_t last;
    if (safe_last_index(n, &last)) {
        for (size_t i = last + 1; i-- > 0; ) {
            printf("  arr[%zu] = %d\n", i, arr[i]);
        }
    }

    size_t empty_last;
    printf("  খালি array-তে safe_last_index: %s\n",
           safe_last_index(0, &empty_last) ? "ভুলভাবে true দিল!" : "সঠিকভাবে false");
}

/* ── ৫. Overflow-এর empirical সম্ভাবনা ────────────────────────── */

static void demo_probability(void) {
    srand((unsigned)time(NULL));
    long trials = 100000, overflow_count = 0;
    for (long i = 0; i \< trials; i++) {
        uint32_t a = ((uint32_t)rand() << 16) | (uint32_t)rand();
        uint32_t b = ((uint32_t)rand() << 16) | (uint32_t)rand();
        uint32_t out;
        if (checked_add_u32(a, b, &out)) overflow_count++;
    }
    printf("  %ld ট্রায়ালে overflow: %ld (%.1f%%)\n",
           trials, overflow_count, 100.0 * overflow_count / trials);
    printf("  (দুইটা uniform random uint32 যোগে overflow-এর সম্ভাবনা\n"
           "   গাণিতিকভাবে প্রায় ৫০%% — কেন, নিজে প্রমাণ করার চেষ্টা করুন)\n");
}

int main(void) {
    printf("== checked addition টেবিল ==\n");
    demo_table();

    printf("\n== নিরাপদ reverse loop ==\n");
    demo_safe_loop();

    printf("\n== overflow-এর empirical সম্ভাবনা ==\n");
    demo_probability();

    return 0;
}

নিজে বাড়ান:

  1. demo_probability-র “প্রায় ৫০%” দাবিটা নিজে প্রমাণ করুন: দুইটা uniform random uint32_t a, b-এর যোগফল 2³²-এর চেয়ে বড় বা সমান হওয়ার সম্ভাবনা ঠিক কত? (ইঙ্গিত: a + b-এর সম্ভাব্য মান [0, 2^33 - 2]-এর মধ্যে uniform নয়, একটা triangular distribution — সর্বোচ্চ ঘনত্ব মাঝখানে।)
  2. checked_add-এর subtraction সংস্করণ (checked_sub) লিখুন, যেটা a \< b হলে borrow শনাক্ত করে।
  3. size_t-ভিত্তিক একটা ছোট circular buffer লিখুন (push, pop, capacity power-of-two) আর দেখান % এর বদলে & ব্যবহার করে কতটা দ্রুত হয় — number-systems লেসনের benchmark পদ্ধতি অনুসরণ করে।

বাস্তব সিস্টেমে

এই গণিত যেখানে যেখানে সত্যিই ম্যাটার করে

প্রতিটা size_t-চালিত loop। std::vector, std::string, C-এর strlen/malloc — সবখানে length/size size_t। CERT C++ Coding Standard-এর CTR52-CPP-জাতীয় নিয়মগুলো, আর প্রতিটা সিরিয়াস static analyzer (Clang-Tidy, PVS-Studio, Coverity), নির্দিষ্টভাবে “unsigned loop underflow” প্যাটার্ন খুঁজে বের করে — এতটাই সাধারণ এই bug।

C++ Core Guidelines-এর signed/unsigned বিতর্ক। ES.100–ES.107 নিয়মগুলো সরাসরি এই লেসনের সমস্যা মোকাবিলায় লেখা — “don’t mix signed and unsigned arithmetic”, “avoid unsigned arithmetic that isn’t logically ‘bit manipulation’”। Bjarne Stroustrup নিজে পরবর্তী বক্তৃতায় স্বীকার করেছেন std::vector::size()-এর unsigned রিটার্ন টাইপ একটা ঐতিহাসিক ভুল ছিল বলে অনেকে মনে করেন।

Java-র “কোনো unsigned টাইপ নেই” সিদ্ধান্ত। James Gosling ইচ্ছাকৃতভাবে Java-তে unsigned integer বাদ দিয়েছিলেন, ঠিক এই ধরনের bug এড়াতে। ফল: byte Java-তে signed (-128127), আর “unsigned byte”-এর প্রয়োজন হলে প্রোগ্রামাররা & 0xFF idiom ব্যবহার করেন। Java 8 (2014)-এ Integer.toUnsignedLong()-এর মতো helper method যোগ করা হয়, কিন্তু ভাষায় সত্যিকারের unsigned টাইপ কখনোই আসেনি।

Rust-এর ডিজাইন সিদ্ধান্ত। ঠিক এই bug class-এর ইতিহাস দেখে Rust debug build-এ overflow-এ panic! করার সিদ্ধান্ত নেয় (release build-এ পারফরম্যান্সের জন্য wrap করে, কিন্তু wrapping_add/checked_add/saturating_add মেথড দিয়ে explicit নীতি বেছে নিতে হয়) — বিস্তারিত লেসন ৬-এ।

IPv4 TTL field। ৮-bit unsigned। রাউটার প্রতি hop-এ এটা কমায়; যদি এটা 0-তে পৌঁছে যায় (কমানোর আগে চেক করা হয়, তাই underflow ঘটে না) packet ফেলে দেওয়া হয় — এটাই infinite routing loop প্রতিরোধ করে। এই field intentionally unsigned, আর তার wraparound এড়ানোর জন্য protocol design-এ pre-check বসানো হয়েছে — ঠিক এই লেসনের “check before, not after” নীতি।

JavaScript-এর >>> 0 idiom। JS-এর সব সংখ্যা আসলে IEEE 754 double, কিন্তু bitwise operator-গুলো (&, |, >>>) ৩২-bit integer-এ কাজ করে। x >>> 0 লিখে প্রোগ্রামাররা একটা সংখ্যাকে জোর করে unsigned 32-bit representation-এ আনেন — >>> (unsigned right shift) হলো একমাত্র operator যেটা ফলাফলকে unsigned হিসেবে interpret করে।

Hash table capacity। Java HashMap, Python dict, C++ unordered_map — সবার bucket index হিসাব hash & (capacity - 1) বা hash % capacitycapacity == 0 হলে এই একই “n - 1” wraparound সমস্যা আসে, তাই প্রতিটা বাস্তব implementation minimum capacity (সাধারণত 1 বা 8) নিশ্চিত করে রাখে।

Ring buffer / lock-free queue। DPDK, Linux kernel-এর kfifo, audio driver-এর circular buffer — সবাই unsigned index আর power-of-two capacity ব্যবহার করে যাতে wraparound-ই স্বাভাবিক, কাম্য আচরণ হয়ে যায় (idx & (capacity - 1)), যা আমরা number-systems লেসনেও দেখেছি।

যে ভুলগুলো সবাই করে

“Unsigned integer overflow একটা bug, ঠিক signed overflow-এর মতোই।”

সম্পূর্ণ ভুল একটা সমতা। C/C++ standard পরিষ্কারভাবে আলাদা আচরণ করে দুটোর সাথে:

UnsignedSigned
Overflow-এর ফলাফলসংজ্ঞায়িত — mod 2ⁿ wrapUndefined Behavior
Compiler optimize করতে পারে কিনা — আচরণ fixedহ্যাঁ — compiler ধরে নেয় এটা কখনো ঘটে না
একই কোড দুই compiler-এএকই ফলাফলভিন্ন ফলাফল হতে পারে

এই পার্থক্যটাই পরের লেসনের কেন্দ্রীয় বিষয়। Unsigned wraparound “safe” শব্দটার একটা টেকনিক্যাল অর্থে সত্য (predictable, portable), কিন্তু “correct” নয় — আপনার প্রোগ্রামের logic যদি wraparound আশা না করে, ফলাফল এখনও ভুল, শুধু কম্পাইলার তার সুযোগ নিয়ে আরও ভুল করবে না

“`if (a + b < a) overflow` চেকটা সব integer টাইপের জন্য কাজ করে।”

শুধু unsigned-এর জন্য নির্ভরযোগ্য, কারণ unsigned addition-এর “monotonicity” (b \geq 0 \Rightarrow a + b \geq a) ভাষার সংজ্ঞা অনুযায়ী গ্যারান্টিযুক্ত।

int (signed)-এর জন্য একই চেক লিখলে (if (a + b \< a)) সেটা নিজেই undefined behavior invoke করতে পারে — কারণ overflow হওয়ার মুহূর্তেই (গণনার সময়) UB ঘটে যায়, ফলাফল পড়ার আগেই। Compiler তখন পুরো if branch-টাই মুছে ফেলতে পারে, ধরে নিয়ে “signed overflow তো কখনো হয়ই না”। লেসন ৬-এ এই আচরণটা সরাসরি কম্পাইল করে দেখব।

“size_t সবসময় ব্যবহার করা 'নিরাপদ' পছন্দ, যেহেতু আকার কখনো ঋণাত্মক হয় না।”

এই লেসনের size() - 1 উদাহরণটাই এর সরাসরি খণ্ডন। size_t টাইপ-সিস্টেমে “ঋণাত্মক হতে পারে না” জোর করে, কিন্তু বাস্তব জগতে মধ্যবর্তী গণনা মাঝেমধ্যে ঋণাত্মক হওয়ার দরকার পড়ে (এখানে, খালি container-এর “শেষ index”)। ফলাফল ঋণাত্মক না হয়ে বিশাল ধনাত্মক হয়ে যায় — যেটা silent ভাবে array bound-এর বাইরে চলে যায়, কোনো ধরনের সতর্কতা ছাড়াই যদি না compiler flag চালু থাকে।

নিরাপদ নিয়ম: unsigned শুধু তখনই ব্যবহার করুন যেখানে wraparound-ই কাম্য semantics (bitmask, checksum, hash, modular counter)। “শুধু একটা আকার/count” রাখতে হলে, বিয়োগ জড়িত থাকলে আগে থেকেই zero-check করুন, অথবা signed টাইপ বিবেচনা করুন।

বুঝেছেন কি না দেখুন

1

uint8_t-এ 137 + 200 কত হবে? বাইনারি addition দেখিয়ে (carry column সহ) হাতে-কলমে প্রমাণ করুন, তারপর mod 256-এর সাথে মিলিয়ে দেখুন।

প্রয়োগ

গাণিতিক যোগফল: 137 + 200 = 337

বাইনারি:

  137 = 1000 1001
+ 200 = 1100 1000
------------------
 1 0101 0001    ← ৯ bit ফলাফল

কলাম ধরে ধরে (ডান থেকে বাঁয়ে):

bit76543210
a10001001
b11001000
carry-in10100000
sum01010001
carry-out11010000

সবচেয়ে বাঁয়ের carry-out 1 — MSB-এর বাইরে উপচে পড়েছে, তাই overflow ঘটেছে। নিচের ৮টা bit যা থাকে: 0101 0001 = 81

mod দিয়ে যাচাই: 337 \bmod 256 = 337 - 256 = 81

উত্তর: uint8_t-এ 137 + 200 = 81, আর carry-out = 1 (overflow ঘটেছে)।

(uint8_t)(a + b) \< a চেক করলেও একই সিদ্ধান্ত পাওয়া যায়: 81 \< 137 সত্য — overflow শনাক্ত।

2

uint32_t-এ 5u - 8u করলে কেন একটা বিশাল সংখ্যা (4294967293) পাওয়া যায়, আর এটাকে কেন “bug” না বলে “সংজ্ঞা” বলা উচিত?

যুক্তি

uint32_t ℤ_(2^32)-এর একটা প্রতিনিধিত্ব — মান 0 থেকে 4,294,967,295 পর্যন্ত, আর arithmetic সবসময় mod 2^32 reduce হয়।

গাণিতিকভাবে 5 - 8 = -3। কিন্তু -3 কোনো uint32_t মান নয় — uint32_t-তে ঋণাত্মক সংখ্যার জন্য কোনো bit pattern বরাদ্দ নেই। তাই ভাষা (C standard, §6.2.5 ¶9) বলে দেয়: ফলাফল mod 2^32 reduce হবে।

3mod232=2323=42949672963=4294967293-3 \bmod 2^{32} = 2^{32} - 3 = 4294967296 - 3 = 4294967293

এটা “bug” না বলার কারণ: এই আচরণ

  1. প্রতিটা compiler, প্রতিটা platform-এ identical — কোনো অনিশ্চয়তা নেই, undefined behavior নেই।
  2. standard-এ লিখিতভাবে সংজ্ঞায়িত — implementation-এর ইচ্ছাধীন নয়।
  3. hardware-এর সরাসরি প্রতিফলন — CPU-র ADD/SUB circuit ঠিক n+1-bit precision-এ গণনা করে MSB-এর পরেরটা ফেলে দেয়, এটাই তার স্বাভাবিক আচরণ, কোনো “সংশোধন” প্রয়োগ করা হয় না।

“Bug” শব্দটা প্রযোজ্য হয় যখন প্রোগ্রামার এই আচরণ প্রত্যাশা করেননি — অর্থাৎ তাদের logic ধরে নিয়েছিল বিয়োগ সবসময় ছোট বা সমান ফলাফল দেবে, যা unsigned world-এ সত্য নয়। গণিতটা নিজে সঠিকভাবে কাজ করছে; ভুলটা মানুষের অনুমানে।

3

নিচের কোডে কী কী সমস্যা আছে, আর কীভাবে ঠিক করবেন?

void print_reverse(const std::vector<int>& v) {
    for (size_t i = v.size() - 1; i >= 0; i--) {
        std::cout << v[i] << " ";
    }
}
প্রয়োগ

দুইটা আলাদা সমস্যা:

সমস্যা ক — v.size() == 0 হলে তাৎক্ষণিক বিপর্যয়। 0 - 1 unsigned world-এ mod 2⁶⁴ wrap করে SIZE_MAX-এ। প্রথম iteration-ই v[SIZE_MAX] access করে — undefined behavior।

সমস্যা খ — i >= 0 তুলনাটা কখনো মিথ্যা হয় না। size_t unsigned, তাই সবচেয়ে ছোট সম্ভাব্য মান 0, আর 0 >= 0 সত্য। তাই i যখন 0-এ নামে, তার পরের i-- আবার wrap করে SIZE_MAX-এ পাঠায়, আর loop চলতেই থাকে — অসীম লুপ (বাস্তবে কোনো এক সময় out-of-bounds access crash ঘটাবে)।

ফিক্স — postfix trick:

void print_reverse(const std::vector<int>& v) {
    for (size_t i = v.size(); i-- > 0; ) {
        std::cout << v[i] << " ";
    }
}

কেন এটা কাজ করে: i-- > 0 এক্সপ্রেশনে তুলনা আগে পুরনো মান দিয়ে হয়, তারপর i কমে। v.size() == 0 হলে i = 0, 0 > 0 মিথ্যা — loop body একবারও চলে না, কোনো wrap ঘটার সুযোগই নেই। v.size() == 3 হলে i যায় 3 → (তুলনা 3>0, i=2, ব্যবহার হয় 2) → (তুলনা 2>0, i=1, ব্যবহার হয় 1) → (তুলনা 1>0, i=0, ব্যবহার হয় 0) → (তুলনা 0>0 মিথ্যা, থামে)

বিকল্প ফিক্স — reverse iterator (আধুনিক C++-এ পছন্দনীয়):

void print_reverse(const std::vector<int>& v) {
    for (auto it = v.rbegin(); it != v.rend(); ++it) {
        std::cout << *it << " ";
    }
}

এখানে index-এর প্রশ্নই ওঠে না — rbegin()/rend() নিজেই সঠিকভাবে খালি container সামলায়।

4

আপনি একটা arcade-স্টাইল গেমের score counter ডিজাইন করছেন, uint32_t-এ রাখা। একজন player যদি কোনোভাবে score বাড়াতেই থাকে (bug বা exploit দিয়ে), কী ঘটবে? বাস্তব গেম ইতিহাসে এমন কিছু ঘটেছে কি? আপনি কীভাবে ডিজাইন করবেন যাতে সমস্যাটা না হয়?

ডিজাইন

কী ঘটবে: uint32_t mod 2^32-এ চলে, range 0 থেকে 4,294,967,295। Score যদি এই সীমা পার হয়, এটা wrap করে আবার 0-এর কাছাকাছি ফিরে যাবে — player-এর কাছে দেখতে হবে score হঠাৎ শূন্য বা খুব ছোট হয়ে গেছে, যদিও সে আরও বেশি “অর্জন” করেছিল।

বাস্তব উদাহরণ — Pac-Man kill screen (1981)। যদিও এটা সরাসরি score counter নয়, একই শ্রেণির bug: Pac-Man-এর level counter 1-byte (unsigned, 0255) রাখা হয়েছিল, ডিজাইনাররা কখনো ভাবেননি কেউ level 255-এর বেশি পৌঁছাবে। Level 256-এ পৌঁছালে internal byte counter overflow করে, আর “fruit display” routine-টা ভেঙে অর্ধেক স্ক্রিন এলোমেলো অক্ষর-চিহ্নে ভরে যায় — এখন এটা “kill screen” নামে বিখ্যাত, খেলা যায় না।

Diablo 2-তে ডিজাইনাররা সচেতনভাবে gold-এর ক্যাপ বসিয়েছিলেন ঠিক 2,147,483,647 (INT32_MAX)-এ — একটা defensive সিদ্ধান্ত, ঠিক এই ধরনের overflow এড়ানোর জন্য।

ডিজাইন সমাধান — তিনটা স্তর:

  1. উৎসেই আটকান — checked/saturating arithmetic। Score বাড়ানোর প্রতিটা জায়গায় সরাসরি += ব্যবহার না করে saturating add ব্যবহার করুন: score = min(score + points, UINT32_MAX)। ফলাফল কখনো wrap করবে না, শুধু max-এ আটকে থাকবে।

    uint32_t score_add_saturating(uint32_t score, uint32_t points) {
        uint32_t sum = score + points;
        return (sum \< score) ? UINT32_MAX : sum;   /* overflow হলে clamp */
    }
  2. আকারই বাড়িয়ে দিন। uint64_t ব্যবহার করলে max প্রায় 1.8 × 10¹⁹ — বাস্তবে কোনো খেলোয়াড় জীবনেও সেই সংখ্যক point অর্জন করতে পারবে না, তাই সমস্যাটা ব্যবহারিকভাবে অপ্রাসঙ্গিক হয়ে যায়।

  3. Exploit-এর সম্ভাবনা মাথায় রাখুন। যদি score বাড়ানোর পথ (glitch, duplication bug, সময়ের manipulation) আছে, একা arithmetic ফিক্স যথেষ্ট না — উৎস bug-টাও ঠিক করতে হবে, কারণ যেকোনো একটা overflow-প্রতিরোধ কৌশল “infinite score”-এর সমস্যা সমাধান করে না, শুধু “wrapped score”-এর সমস্যা সমাধান করে।

সর্বোত্তম ডিজাইন: uint64_t (সমস্যাটা ব্যবহারিকভাবে অদৃশ্য করে)

  • saturating arithmetic (defense in depth, uint64_t-ও তাত্ত্বিকভাবে wrap করতে পারে) + মূল exploit বন্ধ করা।
5

x86 CPU-তে carry flag (CF) আর overflow flag (OF) নামে দুইটা আলাদা bit আছে FLAGS register-এ। Unsigned arithmetic-এ কেন শুধু CF-ই matter করে, OF নয়?

যুক্তি

CPU প্রতিটা ADD/SUB-এর পর দুটোই সেট করে — কারণ একই bit pattern একসাথে signed আর unsigned দুইভাবেই interpret করা যায়, আর CPU জানে না কোন interpretation প্রোগ্রামার চাইছেন। তাই সে দুই ধরনের “ভুল হয়েছে” সংকেতই তৈরি করে রাখে, decision programmer/compiler-এর উপর ছেড়ে দেয়।

  • CF (Carry Flag): MSB-এর carry-out — “unsigned interpretation-এ কি range-এর বাইরে গেছে?” এই লেসনে আমরা প্রমাণ করেছি unsigned-এ carry-out = overflow, ঠিক ঠিক।

  • OF (Overflow Flag): MSB-এর ঠিক আগের bit-এর carry-in আর MSB-এর carry-out-এর XOR — “signed interpretation-এ কি range-এর বাইরে গেছে?” এটা সম্পূর্ণ ভিন্ন গণনা, যা লেসন ৫-এ পুরোপুরি ব্যাখ্যা হবে।

কেন unsigned-এ শুধু CF: কারণ uint32_t-এর মতো টাইপে প্রোগ্রামার/কম্পাইলার শুধু unsigned interpretation নিয়ে মাথা ঘামায়। ফলাফলের bit pattern-কে signed হিসেবে “কী হতো” সেটা প্রাসঙ্গিক নয় — সেই তথ্যটাই কখনো ব্যবহৃত হয় না, তাই compiler unsigned comparison instruction (JB/JAE ইত্যাদি) generate করার সময় শুধু CF পড়ে, OF সম্পূর্ণ উপেক্ষা করে।

উল্টোটাও সত্য: int-এর সাথে কাজ করার সময় compiler signed comparison instruction (JL/JGE) generate করে, যেগুলো OF (আর SF) পড়ে, CF সম্পূর্ণ উপেক্ষা করে।

একই hardware operation, দুইটা independent সংকেত, দুইটা আলাদা “lens”। এটাই এই পুরো মডিউলের কেন্দ্রীয় থিমের প্রথম concrete প্রমাণ: bit pattern একটাই, অর্থ নির্ভর করে আপনি কোন flag/lens দিয়ে পড়ছেন তার উপর। পরের লেসনে (signed integers) আমরা ঠিক এই “একই bits, দুইটা পাঠ” ধারণাটাকে পূর্ণাঙ্গভাবে বিকশিত করব।

এরপর কী

আমরা এখন পর্যন্ত শুধু ধনাত্মক জগতে ছিলাম — 0 থেকে 2ⁿ − 1। কিন্তু বাস্তব প্রোগ্রামে ঋণাত্মক সংখ্যা লাগে সর্বত্র: তাপমাত্রা, ব্যাংক ব্যালেন্স, অবস্থানের offset, audio waveform। একই n bit দিয়ে কীভাবে ঋণাত্মক সংখ্যাও এনকোড করা যায়?

এই প্রশ্নের উত্তর ইতিহাসে তিনবার ভিন্নভাবে দেওয়া হয়েছিল — sign-magnitude, one’s complement, আর অবশেষে two’s complement, যেটা আজ প্রায় সর্বজনীন (এমনকি C++20 standard-এ এখন এটাই একমাত্র বৈধ representation হিসেবে বাধ্যতামূলক)।

পরের লেসনে আমরা তিনটাই পাশাপাশি রাখব, দেখব কেন প্রথম দুইটাতে “দুইটা শূন্য” নামের একটা বিব্রতকর সমস্যা ছিল, আর কীভাবে two’s complement সেটা সমাধান করে একই সাথে hardware-কেও সরল করে দেয় — একটামাত্র adder circuit দিয়েই যোগ আর বিয়োগ দুটোই। আর আমরা সরাসরি দেখাব: two’s complement negation আসলে ঠিক সেই একই 2ⁿ − x mod 2ⁿ সূত্র যা এই লেসনে বিয়োগ ব্যাখ্যা করতে ব্যবহার করেছি — number-systems লেসনের “arithmetic mod 2ⁿ” থিমটা এবার তার সম্পূর্ণ রূপে ফিরে আসবে।

আরও পড়ুন

  • Computer Systems: A Programmer's Perspective, Chapter 2 — Representing and Manipulating Information — Randal E. Bryant, David R. O'Hallaron · Unsigned encoding ও arithmetic-এর প্রামাণ্য আলোচনা
  • ISO/IEC 9899:2018 (C17), §6.2.5 ¶9 — ISO/IEC JTC1/SC22/WG14 · 'A computation involving unsigned operands can never overflow' — মূল সংজ্ঞা
  • C++ Core Guidelines, ES.100–ES.107 — Bjarne Stroustrup, Herb Sutter (eds.) · signed/unsigned মেশানো নিয়ে বাস্তব, প্রামাণিক নির্দেশনা