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

vector ও array — পাশাপাশি সাজানো ডেটা

std::vector & std::array — Data Laid Out Side by Side

Taught in Bangla, this lesson covers contiguous containers in C++: pitfalls of C-style arrays, std::array, std::vector and its growth, [] versus .at(), inserting and erasing, iterator invalidation, 2D data, and passing data with std::span.

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

  • কেন পাশাপাশি সাজানো মেমরি দ্রুত, আর C-ধাঁচের array-এর তিনটি পুরোনো ফাঁদ
  • std::array আর std::vector — কখন কোনটি, আর push_back, emplace_back, reserve, clear কী করে
  • size বনাম capacity, vector কীভাবে বাড়ে, আর কেন গড়ে push_back সস্তা
  • [] বনাম .at(), মাঝখানে ঢোকানো-মোছার খরচ, আর push_back-এর পরে reference ভেঙে যাওয়ার বাগ
  • দুই মাত্রার ডেটা, ফাংশনে vector পাঠানো, std::ranges::sort, std::accumulate আর C++20-এর std::span

১ · মডিউল ৩ শুরু — কেন পাশাপাশি সাজানো ডেটা এত দ্রুত

মডিউল ২-এ আপনি ভাষার যন্ত্রপাতি হাতে নিয়েছেন — অপারেটর, লুপ, ফাংশন, reference, নিজের টাইপ। কিন্তু এতদিন প্রোগ্রাম কাজ করেছে একটি-দুটি মান নিয়ে। বাস্তবের ডেটা আসে তালিকায়: একটি দোকানের মাসের ৩০ দিনের বিক্রি, একটি ক্লাসের ষাট জনের নম্বর, একটি সিনেমা হলের শখানেক সিট। মডিউল ৩ “মেমরি, স্ট্রিং আর রিসোর্স” এই প্রশ্ন দিয়েই শুরু: অনেকগুলো মান মেমরিতে কোথায়, কীভাবে থাকে — আর কে সেই মেমরির দায়িত্ব নেয়?

পর্ববিষয়মূল প্রশ্ন
১৩vector ও arrayঅনেকগুলো মান এক সারিতে রাখা
১৪string ও string_viewলেখা আসলে অক্ষরের একটি সারি
১৫–১৬pointer, new/deleteঠিকানা দিয়ে কাজ, আর হাতে মেমরি নেওয়ার বিপদ
১৭–১৮RAII, smart pointer, lifetimeমেমরির মালিক কে, আর কে কতক্ষণ বাঁচে
১৯মাঝপথের প্রজেক্টসব একসাথে — একটি CLI টুল

এই পুরো মডিউলের কেন্দ্রে একটি সরল ধারণা: উপাদানগুলো মেমরিতে পাশাপাশি থাকলে কাজ দ্রুত হয়। একে বলে contiguous memory. ধরুন একটি int ৪ বাইটের, আর প্রথম উপাদানটি আছে ঠিকানা 1000-এ। তাহলে দ্বিতীয়টি 1004-এ, তৃতীয়টি 1008-এ — সূচক i-এর উপাদান ঠিক 1000 + 4 × i-এ। কোনো খোঁজাখুঁজি নেই, একটি গুণ আর একটি যোগ — তাই যেকোনো উপাদানে পৌঁছানো এক লাফে।

কমলাপুর থেকে ছাড়া একটি ট্রেনের কথা ভাবুন। বগিগুলো পরপর জোড়া, প্রতিটি বগির নম্বর আছে। “ঙ” বগি খুঁজতে আপনাকে পুরো স্টেশন ঘুরতে হয় না — প্রথম বগি থেকে গুনে গেলেই হয়, আর প্ল্যাটফর্মের বোর্ডেই লেখা থাকে কোন বগি কোথায় থামবে। এবার কল্পনা করুন, বগিগুলো শহরের নানা জায়গায় ছড়ানো, আর প্রতিটি বগিতে শুধু লেখা আছে “পরেরটি অমুক জায়গায়”। একটি একটি করে খুঁজতে খুঁজতেই দিন শেষ। প্রথমটি array/vector, দ্বিতীয়টি linked list.

গতির আরেকটি, আরও বড় কারণ আছে — CPU-র cache. মূল মেমরি (RAM) CPU-র তুলনায় অনেক ধীর। তাই CPU মেমরি থেকে একটি একটি বাইট আনে না, একসাথে একটি ছোট টুকরো আনে (সাধারণ কম্পিউটারে ৬৪ বাইটের একটি cache line) আর কাছের দ্রুত cache-এ রাখে। পাশাপাশি সাজানো ১৬টি int একটি cache line-এই এঁটে যায়: প্রথমটি পড়তে গিয়ে বাকি ১৫টিও বিনা খরচে হাতে চলে এল। আর আপনি যদি সারি ধরে এগোতে থাকেন, CPU আগেভাগেই পরের টুকরো এনে রাখে। এই সুবিধার নাম cache locality. বাস্তব প্রোগ্রামে এটি প্রায়ই Big-O হিসাবের চেয়েও বেশি পার্থক্য গড়ে দেয় — ভাবনার প্রশ্ন ০১-এ এর একটি বিখ্যাত উদাহরণ দেখবেন।

মূল ধারণা

C++-এ পাশাপাশি সাজানো ডেটার তিনটি রূপ আছে: পুরোনো C-ধাঁচের array, নির্দিষ্ট আকারের std::array, আর দরকারমতো বাড়তে পারা std::vector. মেমরিতে তিনটির ছবি একই — ফাঁকহীন সারি। পার্থক্য হলো, কে আকার মনে রাখে, কে সীমা পাহারা দেয়, আর কে মেমরির দায়িত্ব নেয়। আধুনিক C++-এর উত্তর: প্রায় সবসময় vector, আকার নির্দিষ্ট হলে std::array, আর C-array প্রায় কখনো না।

২ · C-ধাঁচের array — তিনটি পুরোনো ফাঁদ

C থেকে C++ পেয়েছে C-style array: int sales[30];. মেমরিতে এটি নিখুঁত — ৩০টি int পাশাপাশি, কোনো বাড়তি খরচ নেই। সমস্যা মেমরিতে নয়, এর আচরণে। পুরোনো কোড পড়তে, আর AI-এর লেখা কোডে ভুল ধরতে, এই তিনটি ফাঁদ চিনে রাখা জরুরি।

ফাঁদ ১ — নিজের আকার জানে না

array-এর আকার শুধু সেই জায়গাতেই জানা যায় যেখানে এটি ঘোষণা হয়েছে। সেখানে sizeof(sales) / sizeof(sales[0]) লিখে বা C++17-এর std::size(sales) দিয়ে ৩০ পাওয়া যায়। কিন্তু array-কে কোথাও পাঠালেই এই তথ্য হারিয়ে যায়। তাই C-তে প্রায় প্রতিটি ফাংশন দুটি জিনিস নেয় — array আর তার আকার, যেমন average(sales, 30). কেউ ভুল সংখ্যা দিলে কেউ টের পায় না।

ফাঁদ ২ — ফাংশনে গেলে pointer হয়ে যায়

এটি সবচেয়ে বিভ্রান্তিকর। void f(int a[30]) লিখলে মনে হয় ফাংশনটি ৩০টি int-এর array নিচ্ছে। আসলে compiler এটিকে পড়ে void f(int* a) হিসেবে — শুধু প্রথম উপাদানের ঠিকানা। ৩০ সংখ্যাটি পুরো উপেক্ষিত। একে বলে array-to-pointer decay. ফলে ফাংশনের ভেতরে sizeof(a) দেয় একটি pointer-এর আকার (সাধারণ 64-bit সিস্টেমে ৮), ৩০ × ৪ = ১২০ নয়। GCC এতে সতর্ক করে -Wsizeof-array-argument দিয়ে, কিন্তু সতর্কবার্তা পড়েন কয়জন?

void report(int sales[30]) {               // আসলে: int* sales
    auto n = sizeof(sales) / sizeof(sales[0]);   // 8 / 4 = 2, 30 নয়!
}

আরও একটি ফল: C-array-কে = দিয়ে কপি করা যায় না (int b[30] = sales; error), ফাংশন থেকে ফেরতও দেওয়া যায় না, আর a == b লিখলে মান নয়, দুটি ঠিকানা তুলনা হয়। অর্থাৎ int-এর মতো সাধারণ মানের আচরণ সে করে না।

ফাঁদ ৩ — সীমা পাহারা দেয় না

৩০ ঘরের array-এ sales[30] বা sales[-1] লিখলে compiler সাধারণত কিছু বলে না, প্রোগ্রামও থামে না। সেই ঠিকানায় অন্য যা আছে — হয়তো আরেকটি ভ্যারিয়েবল — সেটাই পড়া বা লেখা হয়। এটা undefined behavior: প্রোগ্রাম ঠিক চলতে পারে, ভুল ফল দিতে পারে, crash করতে পারে, বা সপ্তাহখানেক পরে অন্য কোথাও অদ্ভুত আচরণ করতে পারে। ইতিহাসের বহু বড় নিরাপত্তা-ত্রুটি ঠিক এই “সীমার বাইরে লেখা” থেকে এসেছে। পর্ব ০৮-এর off-by-one ভুল (i <= n লেখা i < n-এর বদলে) এখানেই সবচেয়ে বেশি ক্ষতি করে।

C জানলে

C-এর array আর C++-এর C-array হুবহু এক — decay, সীমাহীনতা, আকার হারানো সবই একই। C-তে চলার সময় আকার ঠিক করা VLA (int a[n];) আছে, standard C++-এ নেই (GCC extension হিসেবে মেনে নেয়, কিন্তু নির্ভর করবেন না)। C-তে বাড়তে পারা array বানাতে malloc/realloc আর হাতে হিসাব রাখতে হতো; C++-এর std::vector ঠিক সেই কাজটিই নিরাপদে নিজে করে। মেমরির ঠিকানা, সূচকের হিসাব আর বহু-মাত্রার array গভীরভাবে দেখতে C কোর্সের array আর বহু-মাত্রার array পর্ব দুটি দেখুন।

৩ · std::array — নির্দিষ্ট আকার, কিন্তু নিজের আকার জানে

সপ্তাহে ৭ দিন, বছরে ১২ মাস, দাবার বোর্ডে ৬৪ ঘর — অনেক ডেটার আকার প্রোগ্রাম লেখার সময়েই জানা। এদের জন্য C++11 এনেছে <array> header-এর std::array. এটি আসলে একটি C-array-কে ঘিরে রাখা ছোট একটি struct — মেমরিতে হুবহু একই, এক বাইটও বাড়তি নয়, গতিও একই। কিন্তু আচরণে অনেক ভদ্র:

std::array<int, 7> week{};               // ৭টি int, সব 0
std::array<int, 7> copy = week;           // পুরো কপি — C-array-এ যা অসম্ভব
std::println("{}", week.size());          // 7 — নিজের আকার জানে
if (copy == week) { }                     // মান তুলনা, ঠিকানা নয়
auto days = std::to_array({"Sat", "Sun", "Mon"});   // C++20: আকার নিজেই গুনে নেয়

📏 আকার জানে

.size() সবসময় সঠিক, ফাংশনে পাঠালেও। আকার টাইপেরই অংশ: array<int, 7> আর array<int, 8> দুটি আলাদা টাইপ।

decay নেই

📋 int-এর মতো আচরণ

= দিয়ে কপি, == দিয়ে তুলনা, ফাংশন থেকে ফেরত — সব চলে। একে বলে value semantics.

সাধারণ মান

⚡ বাড়তি খরচ শূন্য

heap লাগে না, সাধারণত stack-এই থাকে। sizeof(array<int, 7>) = sizeof(int[7]).

C-এর সমান গতি

খেয়াল করুন, std::array একটি aggregate — পর্ব ১২-এর struct-এর মতোই {} দিয়ে মান সাজানো যায়, আর week{} লিখলে সব ঘর শূন্য হয়। কিন্তু std::array<int, 7> w; (কোনো {} নেই) লিখলে C-array-এর মতোই আবর্জনা মান থাকে — তাই {} লেখার অভ্যাস রাখুন। আর আকার বাড়ানো-কমানো যায় না: ৮ম দিন যোগ করার কোনো উপায় নেই। আকার যখন ডেটার ওপর নির্ভর করে, তখন দরকার vector.

৪ · std::vector — বাড়তে পারা array, নিজের মেমরির মালিক

একটি চায়ের দোকানের মালিক প্রতিদিনের বিক্রি লিখে রাখতে চান। মাস শেষ হওয়ার আগে জানেন না কয় দিন দোকান খোলা থাকবে — হরতাল বা ঈদের ছুটিতে কম, হয়তো। এখানে দরকার এমন একটি সারি, যা দরকারমতো বড় হয়। সেটাই <vector> header-এর std::vector.

std::vector<int> sales;            // খালি — size 0
sales.push_back(1250);              // শেষে একটি যোগ — size 1
sales.push_back(980);               // size 2
std::vector<int> marks{72, 85, 64}; // শুরুতেই তিনটি মান
std::vector<int> zeros(30);         // ৩০টি 0 — গোল বন্ধনী!
std::vector<int> one{30};           // একটিমাত্র উপাদান: 30 — ফাঁদ!

শেষ দুটি লাইন একটি পরিচিত ফাঁদ: (30) মানে “৩০টি উপাদান”, কিন্তু {30} মানে “একটি উপাদান, যার মান ৩০”। পর্ব ০৫-এ brace initialization-কে নিরাপদ বলেছিলেন; vector-এর ক্ষেত্রে {} আগে মানের তালিকা হিসেবে পড়া হয়, তাই আকার দিতে গোল বন্ধনী লাগে।

vector আসলে কী — তিনটি সংখ্যা আর একটি ব্লক

ভেতরে একটি vector খুবই সরল। এর নিজের শরীরে থাকে তিনটি জিনিস: heap-এ নেওয়া একটি মেমরি ব্লকের শুরুর ঠিকানা, কয়টি উপাদান আসলে আছে (size), আর ব্লকে মোট কয়টির জায়গা আছে (capacity). উপাদানগুলো নিজেরা থাকে সেই ব্লকে — পাশাপাশি, ঠিক array-এর মতো। আর vector সেই ব্লকের মালিক: vector-এর জীবন শেষ হলে (যেমন ফাংশন থেকে বেরোলে) সে নিজেই উপাদানগুলো ধ্বংস করে মেমরি ফেরত দেয়। আপনাকে কখনো delete লিখতে হয় না। এই “মালিক নিজেই পরিষ্কার করে” নীতির নাম RAII — পর্ব ১৭-এ এর পুরো রূপ দেখবেন।

একটি পরিবার ঢাকায় দুই রুমের বাসা ভাড়া নিল। পরিবারে মানুষ বাড়ছে — নতুন শিশু, গ্রাম থেকে আসা আত্মীয়। যতক্ষণ খালি বিছানা আছে (size < capacity), নতুন সদস্য সহজেই ঢুকে যান। বিছানা ফুরালে পরিবারটি বড় বাসা খোঁজে — সাধারণত আগেরটির প্রায় দ্বিগুণ, যাতে বারবার বাসা বদলাতে না হয়। তারপর সব মালপত্র নতুন বাসায় টানা, পুরোনো বাসা মালিককে ফেরত। বাসা বদলানো ঝামেলার, তাই বুদ্ধিমান পরিবার আগেই জেনে নেয় কতজন আসবেন — সেটাই reserve().

vector ঠিক এভাবেই বাড়ে। push_back-এ জায়গা থাকলে নতুন উপাদান সোজা পরের খালি ঘরে বসে — খুব দ্রুত। জায়গা না থাকলে ঘটে reallocation: নতুন বড় ব্লক, সব উপাদান সেখানে সরানো, পুরোনো ব্লক ফেরত। নিচের stepper-এ পাঁচটি push_back ধাপে ধাপে দেখুন। সংখ্যাগুলো এমন একটি লাইব্রেরির, যা প্রতিবার capacity ২ গুণ করে (GCC আর Clang-এর লাইব্রেরি প্রচলিতভাবে এমনই করে); অন্য লাইব্রেরি বা সংস্করণে capacity-র সংখ্যা আলাদা হতে পারে, কিন্তু ছবিটা একই।

1std::vector<int> v;2v.push_back(10);3v.push_back(20);4v.push_back(30);5v.push_back(40);6v.push_back(50);
vector-এর ভেতরের অবস্থা (২ গুণ বাড়ানো লাইব্রেরি)
size
capacity
ব্লক
এই ধাপে সরানো
  1. খালি vector. size 0, capacity 0 — কোনো মেমরি ব্লকই নেওয়া হয়নি। খালি vector প্রায় বিনা খরচে বানানো যায়: শুধু তিনটি সংখ্যা।
  2. প্রথম push_back. জায়গা নেই, তাই vector heap থেকে একটি ব্লক (A) নিল, যাতে একটি int-এর জায়গা আছে, আর সেখানে 10 বসাল।
  3. প্রথম বাসা বদল. A ভরা। নতুন ব্লক B নেওয়া হলো দুটির জায়গাসহ, 10 সরানো হলো, তারপর 20 বসল, আর A ফেরত দেওয়া হলো। size আর capacity দুটোই এখন 2.
  4. আবার দ্বিগুণ. B-ও ভরা। এবার C-তে চারটির জায়গা — দুটি পুরোনো উপাদান সরানো হলো, 30 বসল, একটি ঘর খালি রইল। এই খালি ঘরটিই ভবিষ্যতের জন্য সঞ্চয়।
  5. এবার বাসা বদল নেই. size 3, capacity 4 — খালি ঘর ছিল, তাই 40 সোজা সেখানে বসল। কোনো নতুন ব্লক নেই, কোনো সরানো নেই। বেশিরভাগ push_back এমনই সস্তা।
  6. তৃতীয় বাসা বদল. C ভরা, তাই আটটির জায়গাসহ D. পাঁচটি push_back-এ মোট ১ + ২ + ৪ = ৭টি সরানো হয়েছে। শুরুতে v.reserve(5) লিখলে একটিও হতো না — একবারেই পাঁচটির ঘর নেওয়া হতো।

capacity প্রতিবার কত গুণ বাড়বে, C++ standard তা বলে দেয় না — এটি লাইব্রেরির সিদ্ধান্ত, যাকে বলে growth factor. GCC (libstdc++) আর Clang-এর libc++ সাধারণত ২ গুণ বাড়ায়, Microsoft-এর লাইব্রেরি ১.৫ গুণ। standard শুধু একটি শর্ত দেয়: শেষে উপাদান যোগ করার গড় খরচ ধ্রুবক হতে হবে — amortized O(1). গুণ করে বাড়ালে এটা সম্ভব (কেন, ভাবনার প্রশ্ন ০২-এ দেখবেন); প্রতিবার মাত্র ১টি বা ১০টি ঘর বাড়ালে সম্ভব হতো না।

reserve, emplace_back আর clear

উপাদান কয়টি হবে আগে থেকে জানলে (বা মোটামুটি আন্দাজ থাকলে) reserve(n) লিখুন। এটি size বদলায় না — vector এখনো খালি — শুধু অন্তত nটির ঘর নিয়ে রাখে। struct-এর vector-এ emplace_back কাজে লাগে: orders.emplace_back("Tea", 2) লিখলে নতুন উপাদানটি সরাসরি vector-এর ঘরেই তৈরি হয়, আগে একটি অস্থায়ী বস্তু বানিয়ে তারপর সরাতে হয় না। আর clear() সব উপাদান মুছে size 0 করে, কিন্তু ব্লকটি রেখে দেয় — capacity আগের মতোই। মেমরি সত্যিই ফেরত চাইলে shrink_to_fit() একটি অনুরোধ, যা লাইব্রেরি মানতে বাধ্য নয় (বাস্তবে GCC মানে)। নিচের প্রোগ্রামে দোকানের এক মাসের বিক্রি দেখুন:

#include <print>
#include <vector>

int main() {
    std::vector<int> sales;
    sales.reserve(30);                         // 30 diner jayga age theke
    const auto cap = sales.capacity();

    for (int day = 1; day <= 30; ++day)
        sales.push_back(1000 + day * 50);      // dainik bikri (taka)

    std::println("days = {}, empty = {}", sales.size(), sales.empty());
    std::println("capacity >= 30: {}, new block during month: {}",
                 cap >= 30, sales.capacity() != cap);
    std::println("day 1 = {}, day 30 = {}", sales.front(), sales.back());

    sales.clear();                             // mash shesh, hisab muche felun
    std::println("after clear: size = {}, capacity kept: {}",
                 sales.size(), sales.capacity() == cap);
}

আউটপুটে আসবে days = 30, empty = false, তারপর capacity >= 30: true, new block during month: false — reserve(30) অন্তত ৩০টির জায়গা নিয়েছে, তাই ৩০টি push_back-এ একবারও reallocation হয়নি; standard এটা নিশ্চিত করে (size যতক্ষণ capacity ছাড়ায় না, ততক্ষণ ব্লক বদলায় না)। front() আর back() প্রথম ও শেষ উপাদান দেয়: 1050 আর 2500. শেষে clear()-এর পরে size = 0, কিন্তু capacity kept: true — ব্লকটি রয়ে গেছে, পরের মাসের হিসাব আবার reallocation ছাড়াই শুরু করা যাবে। খেয়াল করুন, আমরা capacity-র আসল সংখ্যা ছাপাইনি, শুধু তুলনা করেছি। কারণ standard শুধু বলে “অন্তত ৩০”; লাইব্রেরি চাইলে একটু বেশিও নিতে পারে, আর এই কোড লেখার সময় পরীক্ষায় দেখা গেছে, GCC-র একেবারে নতুন সংস্করণ সত্যিই ৩০-এর বেশি নেয়। তাই পরীক্ষায় বা টেস্টে নির্দিষ্ট capacity-র সংখ্যার ওপর কখনো ভরসা করবেন না।

৫ · উপাদান পড়া ও বদলানো — [] বনাম .at(), মাঝখানে ঢোকানো-মোছা

vector-এর উপাদান পড়ার দুটি উপায়: v[i] আর v.at(i). সূচক ঠিক থাকলে দুটোই একই মান দেয়। পার্থক্য দেখা যায় শুধু ভুলের সময় — আর ঠিক তখনই পার্থক্যটা সবচেয়ে জরুরি। [] কোনো যাচাই করে না, C-array-এর মতোই সোজা ঠিকানা হিসাব করে পড়ে। .at() আগে bounds checking করে: সূচক size()-এর সমান বা বেশি হলে একটি exception ছোড়ে, যার টাইপ std::out_of_range. ধাপে ধাপে দেখুন:

1std::vector<int> marks{72, 85, 64};2int a = marks[1];3int b = marks.at(2);4int c = marks[3];       // সীমার বাইরে!5int d = marks.at(3);    // সীমার বাইরে!6// g++ -D_GLIBCXX_ASSERTIONS দিয়ে আবার
ভ্যারিয়েবল
marks
a
b
c
  1. তিনটি উপাদান. বৈধ সূচক 0, 1, 2 — অর্থাৎ 0 থেকে size() - 1. সূচক 3 মানে চতুর্থ উপাদান, যা নেই।
  2. [] দিয়ে বৈধ পড়া. marks[1] = 85. কোনো যাচাই নেই, তাই এটি সবচেয়ে দ্রুত — একটি ঠিকানা হিসাব আর একটি পড়া।
  3. .at() দিয়ে বৈধ পড়া. একই কাজ, আগে শুধু একটি তুলনা (2 < 3?)। ফল 64. বৈধ সূচকে দুটির আচরণ হুবহু এক।
  4. [] সীমার বাইরে — নিঃশব্দ বিপদ. marks[3] ব্লকের শেষের পরের মেমরি পড়ে — যা খুশি থাকতে পারে। প্রোগ্রাম কিছু না বলে চলতে থাকে, ভুল মান ছড়িয়ে পড়ে হিসাবে। এটাই undefined behavior: “কাজ করছে” দেখালেও প্রোগ্রাম ভুল।
  5. .at() সীমার বাইরে — জোরালো আওয়াজ. .at(3) দেখল 3 >= 3, তাই std::out_of_range ছুড়ল। কেউ try-catch দিয়ে না ধরায় প্রোগ্রাম থামল, আর বার্তায় ঠিক বলে দিল কোন সূচক, কত আকার। ভুল ধরা পড়ল ঠিক যেখানে ঘটেছে — পরে কোথাও নয়। (exception ধরা আর সামলানো শিখবেন পর্ব ৩৭-এ।)
  6. যাচাই চালিয়ে compile. GCC-র লাইব্রেরিতে -D_GLIBCXX_ASSERTIONS দিয়ে compile করলে []-ও সীমা যাচাই করে, আর ভাঙলে প্রোগ্রাম সাথে সাথে থামায়। Clang-এর libc++-এ একই কাজ করে hardening mode, MSVC-তে debug build. অনুশীলন আর টেস্টের সময় এটা চালু রাখা খুব ভালো অভ্যাস।

তাহলে সবখানে .at() নয় কেন? কারণ বেশিরভাগ লুপে সূচক নিজেই প্রমাণিতভাবে সঠিক — for (std::size_t i = 0; i < v.size(); ++i)-এ i কখনো সীমা ছাড়ায় না, আর range-for-এ তো সূচকই নেই। সেখানে প্রতিবার যাচাই অপ্রয়োজনীয়। একটি ব্যবহারিক নিয়ম: সূচক যখন বাইরে থেকে আসে — ব্যবহারকারীর ইনপুট, ফাইল, নেটওয়ার্ক — তখন .at() বা নিজে যাচাই; যখন লুপের গঠনই সঠিকতা নিশ্চিত করে, তখন [] বা range-for. আর পর্ব ০৫-এর signed-unsigned ফাঁদ মনে রাখুন: v.size() unsigned, তাই int সূচকের সাথে তুলনায় সতর্কবার্তা আসে; C++20-এর std::ssize(v) একটি signed আকার দেয়।

খালি vector-এ v.front(), v.back() বা v[0] — তিনটিই undefined behavior, কারণ কোনো উপাদানই নেই। আর v.size() - 1 খালি vector-এ 0 − 1 হয়ে বিশাল একটি unsigned সংখ্যা দেয় (প্রায় ১৮ কুইন্টিলিয়ন), ফলে for (std::size_t i = 0; i <= v.size() - 1; ++i) খালি vector-এ সীমাহীন দূরে ছুটে যায়। আগে v.empty() যাচাই করুন।

মাঝখানে ঢোকানো আর মোছা — কেন দামি

শেষে যোগ (push_back) আর শেষ থেকে মোছা (pop_back) সস্তা। কিন্তু উপাদানগুলো ফাঁকহীন সারিতে থাকে, তাই মাঝখান থেকে একটি মুছলে ফাঁক রাখা যায় না — পরের সবাইকে এক ঘর বাঁয়ে সরতে হয়। মাঝখানে ঢোকালে উল্টো: পরের সবাইকে এক ঘর ডানে। nটি উপাদানের vector-এ এর খরচ O(n). ক্লাসের পরীক্ষার নম্বরের তালিকা থেকে দ্বিতীয়জনকে বাদ দিলে কী ঘটে দেখুন:

1std::vector<int> m{39, 58, 64, 72, 85};2m.erase(m.begin() + 1);   // 58 মুছুন3//  ভেতরে: m[1] = m[2]4//          m[2] = m[3]5//          m[3] = m[4]6//  শেষ ঘর ধ্বংস, size কমল
ঘরগুলো (সূচক 0 থেকে 4)
m
size
মোট সরানো
  1. পাঁচজনের নম্বর. ফাঁকহীন সারি, সূচক 0 থেকে 4. capacity-র কথা এখানে বাদ রাখি — erase কখনো নতুন ব্লক নেয় না।
  2. 58 মুছতে বলা হলো. erase একটি সূচক নয়, একটি iterator নেয় — m.begin() + 1 মানে “শুরু থেকে এক ঘর পরে” (iterator-এর বিস্তারিত পর্ব ৩২-এ)। সারিতে ফাঁক রাখা যাবে না, তাই পরেরদের সরতে হবে।
  3. প্রথম সরানো. 64 এক ঘর বাঁয়ে এসে 58-কে ঢেকে দিল। এখন সাময়িকভাবে 64 দুবার আছে — কোনো সমস্যা নেই, পরের ধাপে ঠিক হবে।
  4. দ্বিতীয় সরানো. 72 এল সূচক 2-এ।
  5. তৃতীয় সরানো. 85 এল সূচক 3-এ। মুছে ফেলা ঘরের পরে যতজন ছিল, ঠিক ততজনকে সরাতে হলো — এখানে ৩ জন। দশ লক্ষ উপাদানের প্রথমটি মুছলে সরাতে হতো প্রায় দশ লক্ষ।
  6. শেষ ঘর বাদ. শেষের পুরোনো 85 ধ্বংস, size 4. ক্রম অক্ষুণ্ণ। ক্রম না লাগলে একটি চালাক কৌশল আছে: মুছতে চাওয়া ঘরে শেষ উপাদানটি এনে বসিয়ে pop_back() — খরচ O(1). আর একসাথে অনেকগুলো মুছতে C++20-এর std::erase_if (অনুশীলন ৩ দেখুন)।

৬ · পুরোনো ঠিকানা ভেঙে যায় — iterator invalidation

বাসা বদলের গল্পে একটি প্রশ্ন বাকি রইল: পুরোনো বাসার ঠিকানা যাঁদের কাছে ছিল, তাঁদের কী হবে? বন্ধু পুরোনো ঠিকানায় চিঠি পাঠালে সেটা যাবে এমন বাড়িতে, যেখানে এখন অন্য কেউ থাকে — বা বাড়িটাই ভাঙা। vector-এর ক্ষেত্রেও ঠিক তাই। আপনি যদি একটি উপাদানের দিকে reference, pointer বা iterator রেখে দেন, আর তারপর push_back-এ reallocation ঘটে, তাহলে সেই reference এখন মৃত ব্লকের দিকে তাকিয়ে আছে। একে বলে iterator invalidation, আর এটি C++-এর সবচেয়ে ক্লাসিক বাগগুলোর একটি। একটি বাস কাউন্টারের সিট-তালিকা দিয়ে দেখুন:

1std::vector<int> seats{11, 12, 13};2int& first = seats[0];3int* p = &seats[1];4seats.push_back(14);    // জায়গা নেই!5first = 99;             // মৃত ঠিকানায় লেখা6std::size_t idx = 0; seats[idx] = 99;
মেমরির অবস্থা
seats-এর ব্লক
first দেখায়
p দেখায়
  1. তিনটি সিট, ব্লক A. তিনটি মান দিয়ে বানানো vector-এর capacity অনেক লাইব্রেরিতে ঠিক 3 (কিছু নতুন সংস্করণ একটু বেশি নেয়) — এখানে ধরা যাক কোনো খালি ঘর নেই।
  2. প্রথম সিটের দিকে reference. first এখন ব্লক A-এর প্রথম ঘরের আরেক নাম (পর্ব ১০)। এই মুহূর্তে সম্পূর্ণ বৈধ।
  3. দ্বিতীয় সিটের ঠিকানা. p রাখল A-এর দ্বিতীয় ঘরের ঠিকানা। এটাও এখন বৈধ।
  4. নতুন যাত্রী — বাসা বদল. capacity 3, size 3, তাই push_back নতুন ব্লক B নিল (২ গুণ হলে ৬টির জায়গা), সব সরাল, A ফেরত দিল। seats নিজে ঠিকঠাক জানে নতুন ঠিকানা। কিন্তু first আর p-কে কেউ খবর দেয়নি — তারা এখনো A-এর দিকে তাকিয়ে। এরা এখন dangling.
  5. মৃত ঠিকানায় লেখা. first = 99 লিখল ফেরত দেওয়া মেমরিতে — use-after-free, undefined behavior. seats[0] এখনো 11! প্রোগ্রাম হয়তো crash করবে না, হয়তো অন্য কোনো ডেটা নষ্ট করবে। -fsanitize=address দিয়ে compile করলে AddressSanitizer ঠিক এই লাইনটি ধরে দেয়।
  6. সমাধান: ঠিকানা নয়, সূচক. সূচক 0 মানে “যেখানেই থাকুক, প্রথম উপাদান” — বাসা বদলালেও সত্য থাকে। অন্য সমাধান: push_back-এর আগে reserve দিয়ে যথেষ্ট জায়গা নেওয়া, বা push_back-এর পরে reference নতুন করে নেওয়া।

কোন কাজে কী ভাঙে, তার একটি সরল মানচিত্র মনে রাখুন। reallocation ঘটলে (push_back, emplace_back, insert, বা capacity-র চেয়ে বেশি reserve) — সব pointer, reference আর iterator অকেজো। reallocation না ঘটলে push_back শুধু end() iterator ভাঙে, বাকি সব বৈধ থাকে। erase বা মাঝখানে insert সেই জায়গা আর তার পরের সবকিছু ভাঙে, আগেরগুলো ঠিক থাকে। clear() সব উপাদানের reference ভাঙে। সবচেয়ে কুখ্যাত রূপটি হলো range-for-এর ভেতরে একই vector-এ push_back:

for (int x : v)
    if (x % 2 == 0) v.push_back(x);   // UB: লুপের লুকানো iterator ভেঙে যেতে পারে

range-for ভেতরে ভেতরে begin() আর end() iterator ধরে রাখে (পর্ব ০৮)। push_back-এ reallocation হলে সেগুলো মৃত ব্লকের দিকে তাকায়। পর্ব ১৮-এ lifetime আর dangling নিয়ে পুরো একটি পর্ব আছে — আজকের বাগটি সেখানকার প্রথম উদাহরণগুলোর একটি।

৭ · দুই মাত্রার ডেটা — সিনেমা হলের সিট-ম্যাপ

একটি সিনেমা হলে ১০টি সারি (A থেকে J), প্রতি সারিতে ১২টি সিট। টিকিট বিক্রির সফটওয়্যারকে জানতে হবে কোন সিট বিক্রি হয়েছে। সবচেয়ে স্বাভাবিক লেখা “vector-এর vector”:

std::vector<std::vector<int>> hall(10, std::vector<int>(12, 0));  // ১০ সারি × ১২ সিট
hall[2][5] = 1;                       // C সারির ৬ নম্বর সিট বিক্রি

পড়তে সুন্দর, কিন্তু মেমরিতে এর ছবি একটি টেবিল নয়। বাইরের vector-এ থাকে ১০টি ছোট vector-এর শরীর (প্রতিটিতে তিনটি সংখ্যা), আর প্রতিটি সারির ১২টি সিট থাকে heap-এর আলাদা আলাদা ১০টি ব্লকে — হয়তো একটি থেকে আরেকটি অনেক দূরে। অর্থাৎ ১১ বার মেমরি চাওয়া, আর সারি থেকে সারিতে যেতে cache-এর সুবিধা হারানো। প্রতিটি সারির আকারও আলাদা হতে পারে, যা কখনো সুবিধা (ত্রিভুজাকার ডেটা), কখনো বাগের উৎস।

বিকল্প হলো একটিমাত্র “চ্যাপ্টা” vector, যেখানে সারিগুলো একটার পর একটা রাখা — আগে A সারির ১২টি, তারপর B সারির ১২টি। একে বলে row-major বিন্যাস, আর সারি r, সিট c-এর সূচক হিসাব করা হয় একটি সূত্রে:

সূচক = r × কলামের_সংখ্যা + c

hall[r][c]                  →   flat[r * 12 + c]
C সারি (r=2), ৬ নম্বর (c=5)  →   flat[2 * 12 + 5] = flat[29]

flat: [A0 A1 … A11][B0 B1 … B11][C0 C1 C2 C3 C4 C5 …] …
       0 ……… 11     12 …… 23     24          29
দিকvector<vector<int>>চ্যাপ্টা vector<int> + সূত্র
লেখাhall[r][c] — সহজflat[r * cols + c] — একটি ছোট ফাংশনে লুকান
মেমরি চাওয়াসারির সংখ্যা + ১ বার১ বার
মেমরিতেসারিগুলো ছড়ানোপুরো টেবিল পাশাপাশি — cache-বান্ধব
সারির আকারপ্রতিটি আলাদা হতে পারেসব সারি সমান
কোথায় মানায়ছোট টেবিল, অসমান সারি, কনটেস্টে দ্রুত লেখাবড় grid, ছবি, ম্যাট্রিক্স, খেলার বোর্ড

আকার যদি প্রোগ্রাম লেখার সময়েই জানা থাকে (ধরা যাক হলটি কখনো বদলাবে না), তাহলে std::array<std::array<int, 12>, 10>-ও একটি ভালো পছন্দ — এটি মেমরিতে পুরোপুরি পাশাপাশি, কোনো heap-ই লাগে না। আর C++23-এ এসেছে std::mdspan, যা চ্যাপ্টা vector-এর ওপর বসিয়ে m[r, c] লিখে পড়ার সুযোগ দেয় — সূত্রটা সে নিজেই হিসাব করে। (বহু সূচকের [r, c] লেখাও C++23 থেকে বৈধ।)

৮ · ফাংশনে vector, range-for, algorithm আর std::span

পাঠানো — const&; ফেরত — সরাসরি মান

পর্ব ১০-এ দেখেছেন, বড় জিনিস মান হিসেবে পাঠালে পুরো কপি হয়। vector-এর ক্ষেত্রে কপি মানে নতুন ব্লক নেওয়া আর প্রতিটি উপাদান কপি করা — দশ লক্ষ নম্বরের তালিকায় সেটা সত্যিই ধীর। তাই নিয়ম:

  • শুধু পড়বে: double average(const std::vector<int>& marks) — const reference, কোনো কপি নেই।
  • বদলাবে: void addBonus(std::vector<int>& marks) — সাধারণ reference.
  • নতুন vector বানিয়ে দেবে: std::vector<int> passed(const std::vector<int>& all) — সরাসরি মান ফেরত দিন। ভয় নেই: compiler সাধারণত copy elision করে কপি পুরো বাদ দেয়, আর না পারলে move করে — শুধু তিনটি সংখ্যা হাতবদল, উপাদানগুলো যেখানে ছিল সেখানেই থাকে (পর্ব ৩৬)। পুরোনো দিনের মতো “ফলাফল রাখার vector reference দিয়ে পাঠাও” কৌশলের দরকার নেই।

হাঁটা আর তৈরি algorithm

সূচক যখন লাগে না, তখন range-for সবচেয়ে নিরাপদ: for (int m : marks) পড়ার জন্য, for (int& m : marks) m += 5; বদলানোর জন্য, আর struct-এর vector-এ for (const auto& s : students). সীমার বাইরে যাওয়ার কোনো সুযোগই নেই। অনেক সাধারণ কাজ — সাজানো, যোগ, খোঁজা, গোনা — হাতে লুপ না লিখে STL-এর তৈরি algorithm দিয়ে করা যায়। C++20-এর std::ranges::sort(v) পুরো vector সাজায়, আর <numeric>-এর std::accumulate সব যোগ করে। পর্ব ৩৩-এ এমন কয়েক ডজন algorithm দেখবেন; আজ শুধু স্বাদ।

std::span — মালিক নয়, শুধু দেখার জানালা

একটি সমস্যা থেকে যায়। average(const std::vector<int>&) শুধু vector নেয়। কারও কাছে যদি std::array<int, 7> থাকে, বা পুরোনো লাইব্রেরি থেকে আসা C-array, বা vector-এর শুধু শেষ তিনটি উপাদান — তখন? C-ধাঁচে লিখলে average(const int* p, std::size_t n), যেখানে আবার সেই পুরোনো ঝুঁকি: ভুল n. C++20-এর <span> header-এর std::span এই দুটি সংখ্যাকে — শুরুর ঠিকানা আর উপাদানের সংখ্যা — এক বস্তুতে বাঁধে। যেকোনো পাশাপাশি সাজানো ডেটা থেকে নিজে থেকেই বানানো যায়, আকার নিজেই নিয়ে নেয়। আর এটি non-owning view: ডেটার মালিক নয়, কপি করা প্রায় বিনা খরচে, কিছু ফেরত দেয় না। নম্বরের তালিকা দিয়ে সব একসাথে দেখুন:

#include <algorithm>
#include <numeric>
#include <print>
#include <span>
#include <vector>

double average(std::span<const int> marks) {    // vector, array, C-array - sob cholbe
    if (marks.empty()) return 0.0;
    const int sum = std::accumulate(marks.begin(), marks.end(), 0);
    return static_cast<double>(sum) / static_cast<double>(marks.size());
}

int main() {
    std::vector<int> marks{72, 85, 39, 91, 64, 58};
    std::ranges::sort(marks);                      // choto theke boro
    std::println("sorted: {}", marks);
    std::println("lowest = {}, highest = {}", marks.front(), marks.back());
    std::println("class average = {:.2f}", average(marks));
    std::println("top 3 average = {:.2f}", average(std::span(marks).last(3)));
}

প্রথম লাইনে আসবে sorted: [39, 58, 64, 72, 85, 91] — C++23-এ std::println পুরো vector সরাসরি ছাপাতে পারে। সাজানোর পরে front() সবচেয়ে কম (39), back() সবচেয়ে বেশি (91). মোট 409, ছয়জনে গড় 68.17. শেষ লাইনে std::span(marks).last(3) vector-এর শেষ তিনটি উপাদানের (72, 85, 91) একটি জানালা বানাল — কোনো কপি ছাড়াই — আর গড় এল 82.67. একই average ফাংশন একটি std::array বা C-array নিয়েও কাজ করত, কিছু না বদলেই। static_cast<double> দুটি পর্ব ০৫-এর integer division-এর ফাঁদ এড়ায়; accumulate-এর শেষের 0 হলো শুরুর মান, আর এর টাইপ (int) ঠিক করে যোগফল কোন টাইপে জমবে।

span মালিক নয় — তাই এটি std::string_view-এর মতোই (পর্ব ১৪) dangling হতে পারে। std::span<int> s = makeMarks(); লিখলে, যেখানে ফাংশনটি একটি অস্থায়ী vector ফেরত দেয়, লাইন শেষেই vector ধ্বংস হয় আর s মৃত মেমরির দিকে তাকায়। আর span রাখার পরে মূল vector-এ push_back করলে আগের অংশের সেই একই invalidation-এর গল্প। নিয়ম: span মূলত ফাংশনের parameter হিসেবে ব্যবহার করুন, যেখানে ডাকার সময় ডেটা নিশ্চিতভাবে বেঁচে থাকে।

পরীক্ষা ও ইন্টারভিউতে
  • std::vector আর std::array-এর পার্থক্য? — array-এর আকার compile-এর সময় নির্দিষ্ট, heap লাগে না; vector চলার সময় বাড়ে-কমে, উপাদান থাকে heap-এ। দুটোই পাশাপাশি সাজানো, দুটোই নিজের আকার জানে।
  • size আর capacity-র পার্থক্য? — size = এখন কয়টি উপাদান আছে; capacity = নতুন মেমরি না নিয়ে কয়টি রাখা যায়। সবসময় size ≤ capacity.
  • push_back-এর time complexity? — amortized O(1); reallocation-এর ধাপে একবার O(n), কারণ capacity গুণ করে বাড়ে।
  • v[i] আর v.at(i)-এর পার্থক্য? — [] সীমা যাচাই করে না (বাইরে গেলে UB); .at() যাচাই করে std::out_of_range ছোড়ে।
  • std::vector<bool> কেন আলাদা? — standard-এর একটি বিশেষ রূপ, যা প্রতিটি bool এক bit-এ রাখে; ফলে &v[0] সাধারণ bool* নয় আর v[i] সত্যিকারের reference ফেরত দেয় না। অনেকে তাই vector<char> বা std::bitset পছন্দ করেন।

৯ · AI-এর যুগে — raw array, যাচাইহীন সূচক আর হারানো reference

AI টুলগুলো কয়েক দশকের C আর পুরোনো C++ কোড পড়ে শিখেছে, আর কনটেস্ট-ধাঁচের সমাধানও তাদের প্রশিক্ষণে প্রচুর। তাই তাদের লেখায় প্রায়ই ফিরে আসে int arr[100005];, new int[n], আর যাচাইহীন সূচক। এসব কোড প্রায়ই পরীক্ষার ইনপুটে ঠিকঠাক চলে — বাগ লুকিয়ে থাকে ঠিক সেই ইনপুটে, যা কেউ পরীক্ষা করেনি। সবচেয়ে সাধারণ ভুলগুলো:

AI যা করেসমস্যাযা হওয়া উচিত
int* a = new int[n]; ... delete[] a;মাঝখানে return বা exception হলে মেমরি হারায়; delete আর delete[] গুলিয়ে ফেলাstd::vector<int> a(n);
int marks[100]; আর আলাদা int n১০১তম ছাত্র এলেই সীমার বাইরে লেখা; আকার আর ডেটা আলাদা থাকেvector + push_back, বা নির্দিষ্ট হলে std::array
ব্যবহারকারীর দেওয়া সূচকে সরাসরি v[i]ভুল ইনপুটে UB, নিরাপত্তা-ত্রুটিআগে যাচাই, বা .at(i)
auto& top = v[0]; v.push_back(x); use(top);reallocation-এর পরে top danglingসূচক রাখুন, বা push_back-এর পরে নতুন করে নিন
লুপে if (bad) v.erase(v.begin() + i); আর তারপর ++iমোছার পরে পরের উপাদান এই ঘরে আসে, লুপ সেটা লাফিয়ে যায়C++20: std::erase_if(v, pred);
void f(std::vector<int> v) শুধু পড়ার জন্যপ্রতি ডাকে পুরো কপিconst std::vector<int>& বা std::span<const int>
লুপে push_back, অথচ সংখ্যা আগেই জানাবারবার reallocationলুপের আগে reserve(n)

AI-কে কাজ দেওয়ার সময় নিয়মগুলো আগে থেকেই বলে দিন, আর তার নিজের কোড নিজেকেই যাচাই করতে বলুন:

আমি C++23 শিখছি (g++, -Wall -Wextra -D_GLIBCXX_ASSERTIONS). একটি দোকানের এক মাসের দৈনিক বিক্রি (টাকা, পূর্ণসংখ্যা) নিয়ে একটি ছোট প্রোগ্রাম লেখো: মোট, গড়, সবচেয়ে বেশি বিক্রির দিন, আর যেসব দিনে বিক্রি ১০০০ টাকার কম সেগুলো তালিকা থেকে বাদ দেওয়া। নিয়ম: ১. কোনো C-ধাঁচের array, new[] বা delete[] নয় — std::vector, আকার নির্দিষ্ট হলে std::array. ২. দিনের সংখ্যা জানা থাকলে push_back-এর আগে reserve করবে। ৩. বাইরে থেকে আসা সূচক (যেমন ব্যবহারকারী যে দিন দেখতে চায়) যাচাই করবে বা .at() ব্যবহার করবে। ৪. শুধু পড়ার ফাংশন std::span<const int> বা const std::vector<int>& নেবে। ৫. বাদ দিতে std::erase_if ব্যবহার করবে, লুপের ভেতরে erase নয়। ৬. push_back-এর আগে নেওয়া কোনো reference, pointer বা iterator পরে ব্যবহার করবে না। কোডের পরে বলো, কোন লাইনে reallocation হতে পারে আর কোন reference সেখানে ভেঙে যেত।

AI-এর লেখা vector/array কোড যাচাইয়ের চেকলিস্ট

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

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

প্র ০১বইয়ে লেখা আছে, linked list-এ মাঝখানে ঢোকানো O(1) আর vector-এ O(n). তবু Stroustrup সহ বহু বিশেষজ্ঞ কেন বলেন “ডিফল্ট হিসেবে vector ব্যবহার করুন”?

Big-O একটি গুরুত্বপূর্ণ হাতিয়ার, কিন্তু এটি শুধু বলে ইনপুট বাড়লে খরচ কোন হারে বাড়ে — প্রতিটি ধাপের আসল খরচ কত, তা বলে না। আধুনিক কম্পিউটারে সেই আসল খরচের বড় অংশ ঠিক করে মেমরি। CPU-র কাছের cache থেকে একটি মান পড়তে লাগে কয়েকটি cycle, আর মূল মেমরি (RAM) থেকে আনতে লাগতে পারে একশো বা তারও বেশি cycle. পাশাপাশি সাজানো ডেটায় একবার মেমরি থেকে আনলে পরের অনেকগুলো উপাদান বিনা খরচে চলে আসে, আর CPU সামনে কী লাগবে আন্দাজ করে আগেভাগে আনতে পারে। linked list-এর প্রতিটি নোড heap-এর এলোমেলো জায়গায়, তাই প্রায় প্রতিটি ধাপে নতুন করে মেমরির অপেক্ষা।

Bjarne Stroustrup ২০১২ সালের GoingNative সম্মেলনে তাঁর বক্তৃতায় একটি বিখ্যাত পরীক্ষার কথা বলেন: এলোমেলো সংখ্যা একটি সাজানো ক্রমে ঢোকাতে হবে, তারপর এলোমেলো জায়গা থেকে একে একে মুছতে হবে। list-এ ঢোকানো O(1), তাই মনে হয় list জিতবে। কিন্তু কোথায় ঢোকাবেন, সেটা খুঁজতে list-কে শুরু থেকে একটি একটি নোড হেঁটে যেতে হয় — সেটাও O(n), আর প্রতিটি পা cache-এর বাইরে। vector-ও খোঁজে O(n), আর সরায় O(n), কিন্তু দুটোই পাশাপাশি মেমরিতে, যা CPU অবিশ্বাস্য দ্রুত করে। ফল: বেশ বড় আকার পর্যন্ত vector অনেক গুণ দ্রুত থাকে। সাথে আরেকটি কারণ: list-এর প্রতিটি নোডে উপাদানের সাথে দুটি pointer আর আলাদা মেমরি চাওয়ার খরচ, তাই একটি int-এর list একটি int-এর vector-এর কয়েক গুণ মেমরি খায়।

এ কারণেই Stroustrup-এর বই আর C++ Core Guidelines দুটোই পরামর্শ দেয়: অন্য কিছু বেছে নেওয়ার স্পষ্ট কারণ না থাকলে vector. সেই কারণ বাস্তবে আছে — যেমন উপাদানের ঠিকানা কখনো বদলানো চলবে না (list-এ insert কোনো reference ভাঙে না), বা দুই প্রান্তেই ঘন ঘন যোগ-মোছা (std::deque), বা চাবি দিয়ে খোঁজা (map, পর্ব ৩১)। কিন্তু সিদ্ধান্তটা আসা উচিত মাপজোখ থেকে, বইয়ের Big-O টেবিল থেকে নয় — পর্ব ৪৯-এ এই মাপার কৌশল শিখবেন।

মূল উপলব্ধি: algorithm-এর জটিলতা আর হার্ডওয়্যারের বাস্তবতা — দুটোই হিসাবে লাগে। আধুনিক CPU পাশাপাশি সাজানো ডেটাকে এত বেশি পুরস্কৃত করে যে “তাত্ত্বিকভাবে খারাপ” vector প্রায়ই “তাত্ত্বিকভাবে ভালো” list-কে হারিয়ে দেয়।

প্র ০২vector capacity ১.৫ বা ২ গুণ করে বাড়ায় কেন — প্রতিবার ঠিক যতটুকু দরকার ততটুকু বা ১০০টি করে বাড়ালে কী সমস্যা? আর কেউ ১.৫, কেউ ২ বেছে নিল কেন?

প্রথমে ধরুন vector প্রতিবার ঠিক এক ঘর বাড়ায়। তাহলে প্রতিটি push_back-এ reallocation, আর kতম push_back-এ k − 1টি উপাদান সরাতে হয়। nটি push_back-এ মোট সরানো ১ + ২ + … + (n − 1), প্রায় n²/২. দশ লক্ষ উপাদানে সেটা প্রায় পাঁচশো বিলিয়ন সরানো — কার্যত অচল। প্রতিবার ১০০টি করে বাড়ালে সরানো ১০০ ভাগের এক ভাগে নামে, কিন্তু হার একই থাকে: O(n²). ধ্রুবক কোনো সংখ্যা যোগ করে এই সমস্যা থেকে বেরোনো যায় না।

এবার গুণ করে বাড়ানো, ধরুন ২ গুণ। capacity যায় ১, ২, ৪, ৮, … করে। nটি উপাদান পর্যন্ত পৌঁছাতে মোট সরানো ১ + ২ + ৪ + … + n/২, যা n-এর চেয়ে কম। অর্থাৎ n টি push_back-এ মোট কাজ ২n-এর কম — গড়ে প্রতি push_back-এ ধ্রুবক খরচ। এটাই amortized O(1): মাঝে মাঝে একটি push_back খুব দামি, কিন্তু সেই দাম আগের অনেকগুলো সস্তা push_back-এর ভেতরে ভাগ হয়ে যায়, যেমন বছরে একবারের বড় বাসা-বদলের খরচ মাসে মাসে ভাগ করে ভাবা যায়। ১.৫ গুণেও একই যুক্তি খাটে, শুধু ধ্রুবকটি একটু বড়। C++ standard তাই কোনো নির্দিষ্ট গুণ বলে না, শুধু শেষে যোগের amortized ধ্রুবক খরচ দাবি করে — যা কার্যত গুণ করে বাড়ানো বাধ্যতামূলক করে।

তাহলে ২ না ১.৫? এখানে একটি সত্যিকারের trade-off. বড় গুণ মানে কম reallocation, কিন্তু বেশি খালি জায়গা নষ্ট হওয়ার ঝুঁকি (২ গুণে সবচেয়ে খারাপ ক্ষেত্রে প্রায় অর্ধেক ব্লক খালি)। আরেকটি সূক্ষ্ম যুক্তি আছে মেমরি পুনর্ব্যবহার নিয়ে: ২ গুণে প্রতিটি নতুন ব্লক আগের সব ফেরত দেওয়া ব্লকের মোট আকারের চেয়ে বড়, তাই পুরোনো ফাঁকা জায়গাগুলো জোড়া লাগিয়েও কখনো নতুন ব্লক বসানো যায় না। গুণটি সোনালি অনুপাতের (প্রায় ১.৬১৮) চেয়ে কম হলে, কয়েক ধাপ পরে ফেরত দেওয়া পুরোনো ব্লকগুলোর জায়গাতেই নতুন ব্লক এঁটে যেতে পারে। Facebook-এর folly লাইব্রেরির fbvector এই যুক্তিতে ১.৫ বেছে নিয়েছিল, আর Microsoft-এর লাইব্রেরিও ১.৫ ব্যবহার করে; GCC আর Clang-এর লাইব্রেরি সরলতা আর কম reallocation-এর জন্য ২ রেখেছে। বাস্তবে memory allocator কীভাবে কাজ করে, তার ওপর নির্ভর করে এই সুবিধা কতটা পাওয়া যায় — তাই বিতর্ক আজও শেষ হয়নি।

মূল উপলব্ধি: “গড় খরচ” আর “প্রতিবারের খরচ” আলাদা জিনিস। গুণ করে বাড়ানো একটি দামি কাজকে বিরল করে গড় খরচ ধ্রুবক রাখে — আর আপনি সংখ্যা আগে জানলে reserve দিয়ে সেই বিরল দামও এড়াতে পারেন।

প্র ০৩C-array তো আগেই ছিল, গতিতেও সমান। তাহলে C++11-এ std::array যোগ করার দরকার পড়ল কেন? আর C-array পুরোপুরি বাদ দেওয়া হলো না কেন?

C-array-এর অদ্ভুত আচরণগুলোর শিকড় ১৯৭০-এর দশকে। Dennis Ritchie C বানিয়েছিলেন B আর BCPL ভাষা থেকে, যেখানে array আসলে একটি pointer-ই ছিল। Ritchie নিজে তাঁর “The Development of the C Language” প্রবন্ধে লিখেছেন, পুরোনো B কোড যেন সহজে চলে, সেজন্য তিনি নিয়ম রাখলেন: array-এর নাম বেশিরভাগ জায়গায় প্রথম উপাদানের pointer-এ বদলে যাবে। সেই এক সিদ্ধান্ত থেকেই এল decay, ফাংশনে আকার হারানো, = দিয়ে কপি করতে না পারা — পঞ্চাশ বছর ধরে অগণিত বাগের উৎস।

C++ এই আচরণ বদলাতে পারত না: C-এর সাথে সামঞ্জস্য রাখা ছিল তার জন্মের শর্ত, আর C-array বদলালে কোটি কোটি লাইন কোড ভেঙে যেত। তাই আবার সেই পরিচিত কৌশল (পর্ব ১২-এর enum class-এর গল্প মনে করুন): পুরোনোটা রেখে পাশে নতুন নিরাপদ রূপ। নিরাপদ রূপটি আসলে বহু বছর আগেই মানুষ নিজেরা বানিয়ে ব্যবহার করছিলেন — Nicolai Josuttis-এর লেখা boost::array নামে Boost লাইব্রেরিতে এটি জনপ্রিয় হয়, আর পরে C++11-এ std::array হিসেবে standard-এ ঢোকে।

এর নকশা চমৎকারভাবে সরল: একটি struct, যার ভেতরে একটিমাত্র C-array member. struct-কে কপি করা যায়, ফেরত দেওয়া যায়, তুলনা করা যায় — তাই ভেতরের array-ও বিনা খরচে সেই ক্ষমতা পেয়ে গেল। struct ফাংশনে গেলে decay হয় না, আর আকারটি টাইপেরই অংশ (N), তাই কখনো হারায় না। আর এটি aggregate, তাই কোনো constructor নেই, মেমরিতে এক বাইটও বাড়তি নেই — Stroustrup-এর “zero-overhead” নীতির একটি নিখুঁত উদাহরণ। সাথে পেল .size(), .at(), iterator, যাতে STL-এর সব algorithm এর ওপর চলে। পরে constexpr সমর্থন বাড়তে থাকায় এটি compile-এর সময়ের হিসাবেও (পর্ব ৩৯) খুব কাজের হয়ে ওঠে।

তাহলে C-array কোথায় টিকে আছে? C লাইব্রেরি আর অপারেটিং সিস্টেমের ইন্টারফেসে, string literal-এ ("Hello" আসলে একটি const char[6]), আর main-এর argv-এ। নতুন কোডে সেগুলোর চারপাশেও আজকাল std::span বসিয়ে কাজ করা হয়। C++ Core Guidelines সরাসরি বলে: নিজের লেখা কোডে C-array-এর বদলে std::array বা vector.

মূল উপলব্ধি: একটি ভালো abstraction সবসময় নতুন ক্ষমতা যোগ করে না — কখনো শুধু পুরোনো জিনিসকে সঠিক আচরণ শেখায়। std::array-এর খরচ শূন্য, লাভ অনেক — তাই এটি না ব্যবহার করার কোনো কারণ প্রায় নেই।

প্র ০৪সীমা যাচাই এত জরুরি হলে C++ কেন v[i]-কে ডিফল্টভাবে যাচাইহীন রাখল? আর আজকের “hardened” standard library এই বিতর্ক কীভাবে বদলাচ্ছে?

C++-এর একটি মৌলিক নীতি “যা ব্যবহার করেন না, তার দাম দেবেন না” (zero-overhead). ১৯৯০-এর দশকে, যখন STL নকশা হচ্ছিল, প্রতিটি সূচক-পড়ায় একটি বাড়তি তুলনা আর শাখা ভেতরের লুপের গতি লক্ষণীয়ভাবে কমাতে পারত। তাই সিদ্ধান্ত হলো দুটি পথ রাখার: দ্রুত [], যেখানে সঠিক সূচক দেওয়া প্রোগ্রামারের দায়িত্ব, আর নিরাপদ .at(), যে চায় তার জন্য। কাগজে যুক্তিটা সুন্দর; বাস্তবে প্রায় সবাই সংক্ষিপ্ত [] লেখেন, আর যাচাইয়ের দায়িত্ব নিঃশব্দে হারিয়ে যায়।

এর দাম পৃথিবী দিয়েছে নিরাপত্তা-ত্রুটিতে। সীমার বাইরে পড়া-লেখা বহু দশক ধরে সবচেয়ে বিপজ্জনক সফটওয়্যার-দুর্বলতার তালিকার ওপরের দিকে থাকে। Microsoft আর Google-এর Chrome দল দুজনেই প্রকাশ্যে জানিয়েছে, তাদের গুরুতর নিরাপত্তা-ত্রুটির বড় অংশ — প্রায় ৭০ শতাংশ — মেমরি-নিরাপত্তার ভুল থেকে আসে। ২০২২-২৪ সালের দিকে মার্কিন সরকারের কয়েকটি সংস্থাও memory-safe ভাষার পক্ষে প্রকাশ্য সুপারিশ দেয়, যা C++ সম্প্রদায়ের ওপর বড় চাপ তৈরি করে।

উত্তরে বিতর্কের ভিত্তিই বদলে গেছে। প্রথম খবর হলো, আধুনিক compiler প্রায়ই প্রমাণ করতে পারে লুপের সূচক সীমার ভেতরে, আর তখন যাচাইটি নিজেই মুছে দেয়; আর CPU-র branch predictor “সবসময় ঠিক আছে” ধরনের শাখা প্রায় বিনা খরচে চালায়। ফলে আসল খরচ আগের ধারণার চেয়ে অনেক কম। Google প্রকাশ করেছে যে তারা নিজেদের বিশাল C++ কোডে libc++-এর hardening চালু করেছে, আর গড় পারফরম্যান্স-খরচ মাপা গেছে প্রায় ০.৩% — বিনিময়ে হাজারের বেশি লুকানো বাগ ধরা পড়েছে। GCC-র লাইব্রেরিতে _GLIBCXX_ASSERTIONS Fedora-র মতো কয়েকটি Linux distribution তাদের প্যাকেজ বানাতে ডিফল্ট হিসেবেই ব্যবহার করে।

standard-ও এগোচ্ছে। C++26-এর জন্য “standard library hardening” নামে একটি প্রস্তাব গৃহীত হয়েছে: implementation একটি hardened মোড দিতে পারবে, যেখানে vector-এর [], front(), span-এর সূচকের মতো জায়গায় precondition ভাঙলে প্রোগ্রাম নিশ্চিতভাবে থামবে — নিঃশব্দে ভুল মেমরি পড়বে না। কোন মোড চালু হবে, কীভাবে চালু হবে, তার খুঁটিনাটি compiler-ভেদে আলাদা হতে পারে, তাই নিজের টুলের নথি দেখে নিন। লক্ষ্য একটাই: পুরোনো কোড এক অক্ষর না বদলেই নিরাপদ হোক।

মূল উপলব্ধি: “গতি বনাম নিরাপত্তা” অনেক সময় মিথ্যা দ্বন্দ্ব — মাপলে দেখা যায় নিরাপত্তার দাম প্রায় শূন্য। নিজের প্রজেক্টে hardened মোড বা assertions চালু রাখুন, আর শুধু মাপজোখে প্রমাণিত গরম লুপে সেগুলো তুলে নেওয়ার কথা ভাবুন।

প্র ০৫std::span তো শুধু একটি pointer আর একটি সংখ্যা — C-এর (int* p, size_t n)-এর চেয়ে এটি সত্যিই কতটা নিরাপদ? আর কোথায় এটি নিজেই বিপদ ডেকে আনে?

মেমরিতে std::span আর (int* p, size_t n) সত্যিই প্রায় একই — দুটি শব্দ। পার্থক্য তথ্যে নয়, কে সেই তথ্য দেয় তাতে। C-ধাঁচে প্রতিটি ডাকার জায়গায় মানুষ হাতে n লেখেন, আর একটি ভুল (sizeof-এর গোলমাল, পুরোনো আকার, ভুল ভ্যারিয়েবল) পুরো ফাংশনকে সীমার বাইরে পাঠায়। span-এ আকার আসে সরাসরি উৎস থেকে — vector-এর size() বা array-এর N থেকে — মানুষের হাত ছোঁয়াই লাগে না। আর p ও n এক বস্তুতে বাঁধা, তাই তারা কখনো আলাদা হয়ে যায় না। ফাংশনের নকশাও পরিষ্কার হয়: average(std::span<const int>) বলে দেয় “আমি শুধু পড়ব, মালিক হব না, যেকোনো পাশাপাশি সাজানো int চলবে।”

span-এর ধারণা এসেছে Microsoft-এর Guidelines Support Library (GSL)-এর gsl::span থেকে, যা C++ Core Guidelines-এর অংশ হিসেবে প্রচলিত হয়েছিল; তারপর C++20-এ std::span হিসেবে standard-এ আসে। কিন্তু standardization-এর সময় একটি গুরুত্বপূর্ণ পরিবর্তন হয়: GSL-এর সংস্করণ সূচক যাচাই করত, standard-এর সংস্করণে [] যাচাইহীন — vector-এর মতোই। এমনকি C++20-এ span-এর .at()-ও ছিল না; সেটি পরে C++26-এ যোগ হয়েছে। অর্থাৎ span “সঠিক আকার” নিশ্চিত করে, কিন্তু আকারের বাইরে সূচক দিলে আবার সেই পুরোনো UB — যদি না hardened মোড চালু থাকে।

বড় বিপদটি অন্য জায়গায়: lifetime. span মালিক নয়, তাই এটি জানে না যে ডেটা এখনো বেঁচে আছে কি না। অস্থায়ী vector থেকে span বানিয়ে রেখে দেওয়া, span রাখার পরে মূল vector-এ push_back করা, বা struct-এর member হিসেবে span রেখে মূল ডেটা আগে ধ্বংস হতে দেওয়া — তিনটিই dangling. compiler এর বেশিরভাগই ধরতে পারে না। এ কারণেই অভিজ্ঞরা পরামর্শ দেন: span (আর string_view) মূলত ফাংশনের parameter হিসেবে, যেখানে ডাকার মুহূর্ত থেকে ফাংশন শেষ হওয়া পর্যন্ত ডেটা বেঁচে থাকার নিশ্চয়তা আছে; দীর্ঘ সময় রেখে দেওয়ার জন্য নয়। Rust-এর মতো ভাষা এই lifetime-এর নিয়ম compiler দিয়েই জোর করে, C++-এ এটা এখনো প্রোগ্রামারের দায়িত্ব — পর্ব ১৮-এ এ নিয়ে বিস্তারিত।

মূল উপলব্ধি: নিরাপত্তার কয়েকটি স্তর আছে — সঠিক আকার, সীমা যাচাই, আর lifetime. span প্রথমটি নিখুঁতভাবে সমাধান করে, দ্বিতীয়টি hardening-এর ওপর ছাড়ে, আর তৃতীয়টি পুরোপুরি আপনার হাতে রাখে। কোনো টুল কোন স্তর ঢাকে, সেটা জানাই নিরাপদ C++-এর আসল দক্ষতা।

অনুশীলন

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

  1. size আর capacity অনুমান: std::vector<int> v; দিয়ে শুরু করে পরপর করা হলো — (ক) v.reserve(50); (খ) ২০টি push_back (গ) v.clear(); (ঘ) v.resize(5);. প্রতিটি ধাপের পরে size কত, আর capacity সম্পর্কে নিশ্চিতভাবে কী বলা যায়?

    (ক) size 0, capacity অন্তত ৫০ — reserve উপাদান বানায় না। (খ) size 20, capacity আগের মতোই (অন্তত ৫০), কোনো reallocation হয়নি, কারণ size কখনো capacity ছাড়ায়নি। (গ) size 0, কিন্তু capacity বদলায়নি — clear ব্লক ফেরত দেয় না। (ঘ) size 5, পাঁচটি উপাদানই 0 (resize নতুন উপাদান ডিফল্ট মান দিয়ে বানায়), capacity এখনো অন্তত ৫০. শিক্ষা: capacity শুধু বাড়ে, নিজে থেকে কমে না; কমাতে চাইলে shrink_to_fit() অনুরোধ।

  2. কোন container? প্রতিটির জন্য std::array, std::vector, না চ্যাপ্টা vector — কারণসহ বলুন: (ক) একটি দোকানের এক মাসের দৈনিক বিক্রি (খ) ভর্তি পরীক্ষায় অংশ নেওয়া সব পরীক্ষার্থীর নম্বর (গ) সপ্তাহের ৭ দিনের নাম (ঘ) ১০ × ১২ সিটের একটি হল, যার আকার কখনো বদলাবে না (ঙ) ব্যবহারকারী যে আকার দেবেন সেই আকারের একটি খেলার বোর্ড।

    (ক) মাসে ২৮ থেকে ৩১ দিন — std::vector (সাথে reserve(31)), অথবা std::array<int, 31> আর আলাদা দিনের সংখ্যা; vector সরল, কারণ size নিজেই দিনের সংখ্যা। (খ) সংখ্যা আগে জানা নেই, লাখ ছাড়াতে পারে — std::vector. (গ) সংখ্যা চিরকাল ৭ — std::array (বা std::to_array). (ঘ) আকার নির্দিষ্ট — std::array<std::array<char, 12>, 10>, পুরোপুরি পাশাপাশি, heap নেই। (ঙ) আকার চলার সময় জানা যায় — চ্যাপ্টা std::vector আর r * cols + c সূত্র, বা ছোট হলে vector<vector>.

  3. বাগ শিকার: একটি AI ৪০-এর কম নম্বর বাদ দিতে লিখেছে — for (std::size_t i = 0; i < v.size(); ++i) if (v[i] < 40) v.erase(v.begin() + i);. v = {35, 38, 72, 20} হলে শেষে v-তে কী থাকবে? কেন? ঠিক করে লিখুন।

    ধাপে ধাপে: i = 0-এ 35 মুছল, v = {38, 72, 20}; তারপর ++i, i = 1 — কিন্তু 38 এখন সূচক 0-এ চলে এসেছে, তাই সে লাফিয়ে গেল! i = 1-এ 72, থাকল। i = 2-এ 20, মুছল, v = {38, 72}. ফল {38, 72} — 38 ভুল করে রয়ে গেল। কারণ erase-এর পরে পরের সব উপাদান এক ঘর বাঁয়ে সরে (erase-এর stepper দেখুন), অথচ লুপ তবু এগিয়ে যায়। এছাড়া প্রতিটি erase O(n), তাই অনেক মোছায় পুরোটা O(n²). সঠিক ও দ্রুত সমাধান (C++20): std::erase_if(v, [](int m) { return m < 40; }); — একবার হেঁটেই সব কাজ, ফল {72}. ([](int m) {...} হলো lambda, পর্ব ৩৪-এ শিখবেন।)

  4. নিজে লিখুন (কোড): উপরের exam_marks.cpp-এ একটি নতুন ফাংশন যোগ করুন — int passCount(std::span<const int> marks), যা ৪০ বা তার বেশি নম্বর পাওয়া কয়জন, তা range-for দিয়ে গুনবে। তারপর পুরো ক্লাসের জন্য, আর std::span(marks).first(3) দিয়ে সবচেয়ে কম তিনজনের জন্য ডেকে ছাপান। এরপর একটি std::array<int, 4> quiz{30, 45, 50, 38}; বানিয়ে একই ফাংশনে পাঠান।

    ফাংশন: int passCount(std::span<const int> marks) { int n = 0; for (int m : marks) if (m >= 40) ++n; return n; }. সাজানোর পরে marks = {39, 58, 64, 72, 85, 91}, তাই পুরো ক্লাসে 5 জন পাস; প্রথম তিনজন {39, 58, 64}-এ 2 জন। quiz-এ {45, 50} পাস — 2. লক্ষ করুন, passCount(quiz) লিখতে কিছুই বদলাতে হয়নি: span নিজে থেকেই std::array থেকে বানানো যায়, আর আকার ৪ নিজেই নিয়ে নেয়। একই ফাংশন vector-এর জন্য const std::vector<int>& নিলে array-টি পাঠানো যেত না। চাইলে C++20-এর std::ranges::count_if দিয়েও এক লাইনে লেখা যায় (পর্ব ৩৩)।

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

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

ক্রমভরা একটি std::vector-এ push_back করলে ভেতরে যা ঘটে, সেগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. দেখা হয় size আর capacity সমান — খালি ঘর নেই
  2. heap থেকে নতুন, বড় একটি মেমরি ব্লক নেওয়া হয় (সাধারণত ১.৫–২ গুণ)
  3. নতুন উপাদান আর পুরোনো সব উপাদান নতুন ব্লকে বসানো হয়
  4. পুরোনো ব্লকের উপাদান ধ্বংস করে ব্লকটি ফেরত দেওয়া হয়
  5. vector নতুন ঠিকানা মনে রাখে — পুরোনো সব reference ও iterator এখন অকেজো

std::vector<int> v; v.reserve(100); এর পরে ১০টি push_back করলে v.size() আর v.capacity() সম্পর্কে কোনটি নিশ্চিতভাবে সত্য?

  • size 110, capacity 110
  • size 10, capacity অন্তত 100
  • size 10, capacity 10
  • size 100, capacity 100

৫টি উপাদানের একটি std::vector<int> v-এ v.at(5) লিখলে কী হয়?

  • শেষ উপাদানটি ফেরত দেয়
  • যাচাই ছাড়া মেমরির পরের ঘর পড়ে — undefined behavior
  • std::out_of_range exception ছোড়ে
  • vector নিজে থেকে বেড়ে ৬টি উপাদানের হয়

একটি সাধারণ 64-bit সিস্টেমে void f(int a[30]) { std::println("{}", sizeof(a)); } কী ছাপায়?

  • 30
  • 120
  • 4
  • 8

std::array<int, 5> সম্পর্কে কোনটি ভুল?

  • push_back দিয়ে চলার সময় এর আকার বাড়ানো যায়
  • = দিয়ে পুরো array কপি করা যায়
  • ফাংশনে পাঠালেও .size() সঠিক থাকে
  • মেমরিতে এটি int[5]-এর সমান আকারের

int& r = v[0]; নেওয়ার পরে v.push_back(x) করতে গিয়ে vector-এর reallocation হলো। এখন r ব্যবহার করলে কী হয়?

  • r নিজে থেকে নতুন ব্লকের প্রথম উপাদানের দিকে সরে যায়
  • r ফেরত দেওয়া পুরোনো মেমরির দিকে তাকায় — ব্যবহার করা undefined behavior
  • Compile error হয়
  • push_back একটি exception ছুড়ে সতর্ক করে

C++20-এর std::span<const int> সম্পর্কে কোনটি সঠিক?

  • এটি ডেটার একটি নিজস্ব কপি রাখে, তাই মূল vector মুছে গেলেও নিরাপদ
  • এর [] সবসময় সীমা যাচাই করে exception ছোড়ে
  • এটি মালিক নয়, আর vector, std::array বা C-array থেকে বানানো যায়
  • এতে push_back দিয়ে উপাদান যোগ করা যায়

শব্দকোষ

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

মনে রাখুন

  1. পাশাপাশি সাজানো ডেটা দ্রুত — ঠিকানার সরল হিসাব আর cache-এর সুবিধা। তাই ডিফল্ট container হলো std::vector.
  2. আকার নির্দিষ্ট হলে std::array, নইলে vector; C-array প্রায় কখনো না — C-array আকার হারায়, pointer-এ বদলে যায়, সীমা পাহারা দেয় না।
  3. size ≤ capacity. জায়গা ফুরালে vector নতুন ব্লকে সব সরায় (গুণ করে বাড়ে, তাই push_back গড়ে O(1)); সংখ্যা জানা থাকলে আগে reserve.
  4. [] যাচাই করে না, .at() করে। বাইরের সূচকে যাচাই, লুপে range-for; মাঝখানে insert/erase O(n), অনেকগুলো মুছতে std::erase_if.
  5. reallocation-এর পরে পুরোনো reference, pointer, iterator মৃত। সূচক রাখুন। ফাংশনে const& বা std::span দিয়ে পাঠান, আর নতুন vector সরাসরি মান হিসেবে ফেরত দিন।

পরের পর্বে: লেখাও আসলে অক্ষরের একটি পাশাপাশি সারি। std::string কীভাবে vector-এর মতোই বাড়ে আর নিজের মেমরির মালিক হয়, ছোট লেখা কেন heap ছাড়াই থাকে, আর std::string_view কেন span-এর মতোই একটি “দেখার জানালা” — সাথে সেই একই dangling-এর সতর্কতা।

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

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