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

string ও string_view — লেখা নিয়ে কাজ

std::string and std::string_view — Working with Text

Taught in Bangla, this lesson covers text in C++: why std::string replaced C strings, building, joining, comparing, find, substr and replace, number conversions and splitting lines, capacity and small-string optimisation, std::string_view, and handling Bangla text as UTF-8.

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

  • মালিক আর দর্শক — std::string আর std::string_view-এর মূল পার্থক্য
  • C string-এর বিপদ, আর std::string কীভাবে সেগুলো দূর করে
  • find, substr, replace, তুলনা, starts_with দিয়ে আসল কাজ — ইমেইল, ফোন নম্বর, রসিদ, CSV লাইন
  • লেখা আর সংখ্যার মধ্যে নিরাপদ রূপান্তর: to_string, stoi, from_chars
  • ভেতরের কথা — capacity, reallocation, SSO; আর বাংলা লেখা কেন byte-এর হিসাবে চলে

১ · মালিক আর দর্শক — এই পর্বের মূল ধারণা

প্রায় সব প্রোগ্রামই লেখা নিয়ে কাজ করে: নাম, ঠিকানা, ফোন নম্বর, ইমেইল, ফাইলের একেকটি লাইন, ওয়েবসাইটের URL. পর্ব ০৭-এ আপনি std::cin আর std::getline দিয়ে লেখা পড়েছেন, আর পর্ব ১০-এ দেখেছেন বড় লেখা ফাংশনে পাঠাতে const std::string&. এবার প্রশ্ন হলো, লেখা মেমরিতে আসলে কীভাবে থাকে, আর কে তার দেখাশোনা করে?

C++-এ লেখা রাখার দুটি প্রধান টাইপ আছে, আর দুটির দায়িত্ব একেবারে আলাদা। std::string হলো লেখার মালিক। আর std::string_view হলো শুধু একজন দর্শক। এই একটি পার্থক্য বুঝলে পর্বের বাকি সবকিছু সহজ হয়ে যায়।

ধরুন আপনার নিজের একটি খাতা আছে। খাতাটি আপনার, তাই আপনি তাতে লিখতে পারেন, পাতা যোগ করতে পারেন, ফটোকপি করে বন্ধুকে দিতে পারেন। খাতা হারানোর দায়ও আপনার। std::string ঠিক এমন খাতা। এবার ক্লাসে আপনার বন্ধু তাঁর খাতা খুলে আঙুল দিয়ে দেখালেন, “এই পাতার তিন নম্বর লাইন থেকে দশটি শব্দ পড়ো।” আপনি পড়তে পারবেন, কোনো কপি লাগবে না। কিন্তু খাতাটি আপনার নয়। বন্ধু খাতা নিয়ে বাসায় চলে গেলে আপনার আঙুল তখন শূন্যে। string_view এই আঙুল-দেখানো।

std::string — মালিক

নিজের অক্ষরগুলো নিজের মেমরিতে রাখে। লেখা বাড়লে নিজেই বড় জায়গা নেয়, কপি করলে সম্পূর্ণ আলাদা কপি দেয়, আর object শেষ হলে মেমরি নিজেই ফেরত দেয়।

লেখা বদলানো, রেখে দেওয়া

std::string_view — দর্শক

ভেতরে শুধু দুটি জিনিস: লেখার শুরুর ঠিকানা আর দৈর্ঘ্য। নিজে কোনো অক্ষর রাখে না, বদলাতেও পারে না। কপি করা মানে ওই দুটি সংখ্যা কপি করা।

শুধু পড়া, সাময়িক
মূল ধারণা

লেখা নিয়ে যেকোনো কোড দেখলে প্রথম প্রশ্ন করুন: এই অক্ষরগুলোর মালিক কে, আর মালিক কতক্ষণ বেঁচে থাকবে? মালিক হলে std::string, শুধু দেখার জন্য std::string_view. আর view যতক্ষণ আছে, মালিককে ততক্ষণ বেঁচে থাকতে হবে। পর্ব ১০-এর reference-এর নিয়মটাই এখানে নতুন চেহারায় এসেছে।

২ · C string থেকে std::string — কেন বদল দরকার হলো

C-তে আলাদা কোনো string টাইপ নেই। লেখা মানে char-এর একটি array, যার শেষে একটি বিশেষ অক্ষর '\0' বসানো থাকে। একে বলে C string, আর শেষের ওই চিহ্নটি হলো null terminator (NUL). "Dhaka" লিখলে মেমরিতে আসলে ৬টি byte থাকে: D h a k a \0. strlen শুরু থেকে গুনতে গুনতে '\0' পেলে থামে।

এই নকশা সরল, কিন্তু এর তিনটি দায়িত্ব পুরোপুরি প্রোগ্রামারের ঘাড়ে:

  • আকার আগে থেকে ঠিক করা। char name[10]; লিখলে সারাজীবন ১০ byte, যার একটি NUL-এর জন্য রাখতে হয়। ব্যবহারকারী ২০ অক্ষরের নাম দিলে কী হবে, আপনাকেই ভাবতে হবে।
  • NUL নিজের হাতে বসানো। কোনো ফাংশন NUL বসাতে ভুলে গেলে strlen বা printf array পেরিয়ে পাশের মেমরি পড়তে থাকে।
  • কপি নিরাপদে করা। strcpy(name, input) কোনো আকার পরীক্ষা করে না। input বড় হলে সে name-এর বাইরে লিখে ফেলে।

শেষেরটিকে বলে buffer overflow, আর এটি কম্পিউটার নিরাপত্তার ইতিহাসে সবচেয়ে বেশি ব্যবহৃত দুর্বলতাগুলোর একটি। ১৯৮৮ সালের বিখ্যাত Morris worm ইন্টারনেটের হাজারো কম্পিউটারে ঢুকেছিল আংশিকভাবে একটি C প্রোগ্রামের gets ফাংশনের এমন ভুলের সুযোগ নিয়ে। gets এতই বিপজ্জনক যে C11 আর C++14 থেকে এটি standard থেকে পুরোপুরি বাদ দেওয়া হয়েছে।

// পুরোনো পথ — C++-এ এভাবে লিখবেন না
char city[8];
std::strcpy(city, "Chattogram");    // ১১ byte লিখল ৮ byte-এর জায়গায়: buffer overflow

// আধুনিক পথ
std::string town = "Chattogram";    // যত লম্বা হোক, নিজেই জায়গা নেয়
town += " Port";                    // বাড়াতে কোনো হিসাব লাগে না
std::string copy = town;            // সম্পূর্ণ আলাদা কপি, নিরাপদ
কাজC string (char[])std::string
দৈর্ঘ্য জানাstrlen(s) — প্রতিবার শুরু থেকে গোনেs.size() — মান জমা থাকে, সাথে সাথে পায়
জোড়া লাগানোstrcat — জায়গা আছে কি না আপনার দায়িত্বs + t, s += t — নিজেই বড় হয়
কপিstrcpy — আকার পরীক্ষা নেইt = s; — নিরাপদ আলাদা কপি
তুলনাstrcmp(a, b) == 0 (a == b ঠিকানা তুলনা করে!)a == b — লেখা তুলনা করে
মেমরি ফেরতmalloc করলে নিজে freeobject শেষ হলে আপনাআপনি

শেষ সারির কৌশলটির নাম RAII — মালিক object শেষ হলে তার রিসোর্সও ফেরত যায়। পর্ব ১৭-এ এটি বিস্তারিত দেখবেন। আপাতত জেনে রাখুন, std::string এই নিয়মের সবচেয়ে পরিচিত উদাহরণ।

C জানলে

ভেতরে std::string-ও অক্ষরগুলো পাশাপাশি রাখে, আর শেষে একটি '\0' রেখে দেয় — তাই s.c_str() দিয়ে যেকোনো C ফাংশনকে নিরাপদে const char* দেওয়া যায়। পার্থক্য হলো, দৈর্ঘ্য আলাদা করে জমা থাকে, তাই লেখার মাঝখানেও '\0' থাকতে পারে, আর মেমরি সামলানো আপনার কাজ নয়। C কোর্সের string পর্বে char array, NUL আর buffer overflow নিয়ে বিস্তারিত আছে; এই পর্বের অংশ ৬-এ দেখবেন std::string ভেতরে সেই একই মেমরি কীভাবে সামলায়।

৩ · প্রতিদিনের কাজ — বানানো, জোড়া, মাপা, তুলনা

std::string পেতে লাগে header <string>. বানানোর কয়েকটি পরিচিত উপায়:

std::string a;                  // খালি লেখা, size() == 0
std::string b = "Rajshahi";     // string literal থেকে
std::string c(5, '*');          // "*****" — একই অক্ষর ৫ বার
std::string d = b + ", BD";     // "Rajshahi, BD"
d += '!';                       // শেষে একটি অক্ষর যোগ

এখানে "Rajshahi" নিজে কোনো std::string নয়। কোডে উদ্ধৃতিচিহ্নের ভেতরে লেখা টেক্সটকে বলে string literal, আর এর টাইপ const char-এর array — C-এর সেই পুরোনো রূপ। std::string তা থেকে একটি নিজস্ব কপি বানিয়ে নেয়। এই কারণে একটি পরিচিত ফাঁদ আছে:

auto s1 = "Dhaka" + "-1205";              // compile error: দুটি ঠিকানা যোগ করা যায় না
auto s2 = std::string("Dhaka") + "-1205"; // ঠিক: বাম পাশে একটি std::string আছে
using namespace std::literals;
auto s3 = "Dhaka"s + "-1205";             // ঠিক: s প্রত্যয় মানে std::string

+-এর অন্তত এক পাশে আসল std::string থাকতে হয়। আর auto x = "Dhaka"; লিখলে x-এর টাইপ হয় const char*, std::string নয় — AI-এর লেখা কোডে এই ভুলটি প্রায়ই দেখা যায়।

মাপা আর একটি অক্ষর পড়া

s.size() (একই কাজ s.length()) বলে লেখায় কয়টি char আছে, আর s.empty() বলে লেখাটি খালি কি না। একটি অক্ষর পড়ার দুটি উপায়, পার্থক্যটা গুরুত্বপূর্ণ:

লেখাসীমার ভেতরেসীমার বাইরে (i >= size())
s[i]অক্ষরটি দেয়, কোনো পরীক্ষা নেই — দ্রুতundefined behavior — কিছুই বলবে না, ভুল জিনিস পড়বে
s.at(i)পরীক্ষা করে অক্ষরটি দেয়std::out_of_range exception ছোড়ে — ভুল সাথে সাথে ধরা পড়ে

ইনপুট বা বাইরের ডেটা থেকে আসা অবস্থান হলে at() নিরাপদ। নিজে লুপ লিখে নিশ্চিত থাকলে []. আর পুরো লেখার প্রতিটি অক্ষর দেখতে সবচেয়ে সহজ পথ পর্ব ০৮-এর range-for: for (char ch : s).

তুলনা — অভিধানের ক্রমে, তবে সংখ্যা দিয়ে

== আর != দুটি লেখার প্রতিটি অক্ষর মেলায়। <, > কাজ করে lexicographic order-এ: বাম থেকে প্রথম যে জায়গায় দুটি লেখা আলাদা, সেখানকার দুটি char-এর সংখ্যা-মান তুলনা হয়। তাই "Dhaka" < "Dinajpur" সত্য ('h' আগে আসে 'i'-এর)। কিন্তু সাবধান: ASCII টেবিলে সব বড় হাতের অক্ষর ছোট হাতের অক্ষরের আগে, তাই "Zebra" < "apple"-ও সত্য। নাম সাজানোর প্রোগ্রামে এটি অবাক করা ফল দেয়।

a.compare(b) এক ডাকেই তিনটি উত্তর দেয়: ঋণাত্মক (a আগে), শূন্য (সমান), ধনাত্মক (a পরে)। C++20-এ এর আধুনিক রূপ <=> অপারেটর, যা পর্ব ০৬-এ দেখেছেন। আর প্রায়ই যে প্রশ্নগুলো করতে হয়, তার জন্য সরাসরি ফাংশন আছে:

std::string url = "https://abcltech.com/cpp-programming/";
url.starts_with("https://");   // true  — C++20 থেকে
url.ends_with("/");            // true  — C++20 থেকে
url.contains("cpp");           // true  — C++23 থেকে

এর আগে একই কাজ করতে হতো url.find("cpp") != std::string::npos বা url.rfind("https://", 0) == 0-এর মতো কঠিন লেখায়। পুরোনো compiler-এ কাজ করতে হলে এগুলো চিনে রাখা ভালো, কিন্তু নতুন কোডে সরাসরি নামগুলোই পরিষ্কার।

৪ · খোঁজা আর কাটা — find, substr, replace

লেখা নিয়ে বাস্তবের বেশিরভাগ কাজ দুই ধাপের: আগে কিছু একটা খুঁজুন, তারপর সেই জায়গায় কাটুন বা বদলান। s.find(x) বাম থেকে খুঁজে প্রথম মিলের অবস্থান (০ থেকে গোনা) দেয়, আর s.rfind(x) খোঁজে ডান দিক থেকে। না পেলে দুটোই ফেরত দেয় একটি বিশেষ মান: std::string::npos. তারপর s.substr(pos, len) অবস্থান pos থেকে lenটি অক্ষরের একটি নতুন substring কপি করে দেয়; len না দিলে শেষ পর্যন্ত।

একটি ইমেইল ঠিকানা থেকে ব্যবহারকারীর নাম আর domain আলাদা করা যাক। প্রতিটি ধাপে আগে নিজে অনুমান করুন, তারপর “পরে” চাপুন:

1std::string email = "rafi.ahmed@abcltech.com";2auto at = email.find('@');3std::string user = email.substr(0, at);4std::string domain = email.substr(at + 1);5auto dot = domain.rfind('.');6auto hash = email.find('#');
ভ্যারিয়েবলের মান
email.size()
at
user
domain
dot
hash
  1. লেখা আর অবস্থান। email-এ ২৩টি অক্ষর। প্রতিটি অক্ষরের একটি অবস্থান আছে, যা গোনা শুরু হয় 0 থেকে। তাই প্রথম অক্ষর r অবস্থান 0-এ, শেষ অক্ষর অবস্থান 22-এ।
  2. find অবস্থান দেয়, অক্ষর নয়। find('@') বাম থেকে খুঁজে প্রথম @ পেল অবস্থান 10-এ। ফলের টাইপ std::size_t — একটি unsigned সংখ্যা, তাই auto লেখাই ভালো।
  3. substr(0, at) — শুরু থেকে at-টি অক্ষর। অবস্থান 0 থেকে 10টি অক্ষর: "rafi.ahmed". @ নিজে বাদ পড়ল, কারণ সেটি অবস্থান 10-এ, আর আমরা নিয়েছি 0 থেকে 9. এটি একটি নতুন std::string — আসল ইমেইলের কপি।
  4. substr(at + 1) — @-এর পর থেকে শেষ পর্যন্ত। দৈর্ঘ্য না দিলে শেষ পর্যন্ত যায়। + 1 না দিলে domain-এর শুরুতে @ চলে আসত — এই এক-ঘর ভুলটি খুব প্রচলিত।
  5. rfind ডান দিক থেকে খোঁজে। "abcltech.com"-এ শেষ . অবস্থান 8-এ। "mail.abcltech.com"-এর মতো একাধিক ডটের ঠিকানায় শেষ অংশ (com) বের করতে rfind-ই ঠিক হাতিয়ার।
  6. না পেলে npos. ইমেইলে # নেই, তাই ফল std::string::npos — 64-bit সিস্টেমে 18446744073709551615, অর্থাৎ size_t-এর সবচেয়ে বড় মান। এটি −1 নয়, আর মিথ্যাও (false) নয়। তাই সবসময় লিখুন if (pos != std::string::npos). ফলটি int-এ রাখলে বা if (pos) লিখলে বাগ নিশ্চিত — অবস্থান 0-তে পাওয়া গেলেও শর্ত মিথ্যা হয়ে যায়।

বদলানো, ঢোকানো, মোছা

তিনটি ফাংশনই আসল string-কে সরাসরি বদলায়, আর প্রথম দুটি আর্গুমেন্ট একই অর্থ বহন করে — কোন অবস্থান থেকে, কয়টি অক্ষর:

std::string s = "Bangladesh Bank";
s.replace(11, 4, "Railway");   // অবস্থান 11 থেকে 4টি অক্ষর বদল: "Bangladesh Railway"
s.insert(0, "The ");           // শুরুতে ঢোকানো: "The Bangladesh Railway"
s.erase(0, 4);                 // প্রথম 4টি অক্ষর মোছা: "Bangladesh Railway"

অবস্থান যদি size()-এর চেয়ে বড় হয়, এই ফাংশনগুলো আর substr সবাই std::out_of_range ছোড়ে। অর্থাৎ find-এর ফল npos কিনা না দেখে সরাসরি substr-এ দিলে প্রোগ্রাম থেমে যেতে পারে। এবার সব একসাথে — ইমেইল ভাগ করা, ফোন নম্বর যাচাই আর একটি রসিদের লাইন বানানো:

#include <print>
#include <string>
#include <string_view>

bool validPhone(std::string_view p) {          // 11 digit, shuru "01" diye
    if (p.size() != 11 || !p.starts_with("01")) return false;
    for (char c : p) if (c < '0' || c > '9') return false;
    return true;
}
int main() {
    std::string email = "rafi.ahmed@abcltech.com";
    auto at = email.find('@');
    if (at != std::string::npos)
        std::println("user = {}, domain = {}", email.substr(0, at), email.substr(at + 1));
    for (std::string_view p : {"01712345678", "0171234567", "0171234567x"})
        std::println("{:<12} -> {}", p, validPhone(p) ? "valid" : "invalid");
    std::string receipt = "Tea x3";
    receipt += " = " + std::to_string(3 * 15) + " Tk";
    std::println("{} ({} chars)", receipt, receipt.size());
}

প্রথম লাইনে আসবে user = rafi.ahmed, domain = abcltech.com — stepper-এর হিসাবই। validPhone তিনটি শর্ত দেখে: দৈর্ঘ্য ১১, শুরু "01", আর প্রতিটি অক্ষর অঙ্ক। তাই প্রথম নম্বরটি valid, দ্বিতীয়টি (১০ অঙ্ক) আর তৃতীয়টি (শেষে x) invalid. এটি শুধু চেহারার যাচাই — নম্বরটি সত্যিই কারও কি না, তা কোনো প্রোগ্রাম এভাবে জানতে পারে না। প্যারামিটার std::string_view, কারণ ফাংশনটি শুধু পড়ে (অংশ ৭)। শেষ লাইনে std::to_string(45) সংখ্যাকে লেখা বানিয়েছে, আর += দিয়ে জোড়া লেগে হয়েছে Tea x3 = 45 Tk (14 chars). খেয়াল করুন, " = " + std::to_string(...) চলেছে, কারণ ডান পাশে একটি আসল std::string আছে।

৫ · লেখা আর সংখ্যা — রূপান্তর ও লাইন ভাগ করা

“21” লেখা আর 21 সংখ্যা মেমরিতে একেবারে আলাদা জিনিস। প্রথমটি দুটি অক্ষর ('2' আর '1', ASCII মান 50 আর 49), দ্বিতীয়টি একটি int. ফাইল, ফর্ম বা নেটওয়ার্ক থেকে সবকিছু আসে লেখা হিসেবে, তাই রূপান্তর প্রতিদিনের কাজ।

কাজফাংশনভুল হলেখেয়াল রাখুন
সংখ্যা → লেখাstd::to_string(45)—double-এ to_string(3.14) দেয় "3.140000"; সুন্দর রূপের জন্য std::format("{}", x)
লেখা → int / doublestd::stoi, std::stodexception: std::invalid_argument বা std::out_of_rangestoi("42abc") নিঃশব্দে 42 দেয় — বাকি অংশ উপেক্ষা
লেখা → সংখ্যা, দ্রুতstd::from_chars (C++17)exception নেই; ফেরত দেয় error code আর থামার জায়গাসামনের স্পেস বা + চিহ্ন মানে না; locale দেখে না

std::stoi ভুল লেখা পেলে একটি exception ছোড়ে — অর্থাৎ স্বাভাবিক পথ থামিয়ে ভুলের খবর পাঠায়। কেউ try { … } catch (…) { … } দিয়ে ধরলে প্রোগ্রাম সামলে নিতে পারে; কেউ না ধরলে প্রোগ্রাম সেখানেই বন্ধ। ব্যবহারকারীর ইনপুট প্রায় কখনো নিখুঁত নয়, তাই stoi ব্যবহার করলে try/catch দিন। Exception নিয়ে পুরো আলোচনা পর্ব ৩৭-এ।

std::from_chars এসেছে উচ্চ গতির প্রয়োজনে — বিশাল JSON বা CSV ফাইল পড়তে। এটি কোনো মেমরি চায় না, exception ছোড়ে না, আর কম্পিউটারের ভাষা-সেটিংস (locale) দেখে না, ফলে সব মেশিনে একই ফল দেয়। দামটা হলো, ফল নিজে যাচাই করতে হয়।

একটি CSV লাইন ভাগ করা

CSV (comma-separated values) ফাইলে প্রতিটি লাইন একটি রেকর্ড, ঘরগুলো কমা দিয়ে আলাদা: Rafi,21,Dhaka. ভাগ করার দুটি প্রচলিত উপায়। প্রথমটি, std::istringstream দিয়ে লেখাটিকে একটি stream বানানো, তারপর পর্ব ০৭-এর std::getline-কে থামার অক্ষর ',' দেওয়া। দ্বিতীয়টি, অংশ ৪-এর মতো find(',') লুপ চালিয়ে প্রতিবার substr কাটা। প্রথমটি ছোট আর পরিষ্কার; দ্বিতীয়টি দ্রুত আর string_view-এর সাথে কপি ছাড়াই করা যায়।

#include <charconv>
#include <print>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>

int main() {
    std::istringstream in("Rafi,21,Dhaka");
    std::vector<std::string> parts;
    for (std::string field; std::getline(in, field, ','); ) parts.push_back(field);
    std::println("{} fields: [{}] [{}] [{}]", parts.size(), parts[0], parts[1], parts[2]);

    int age = 0;
    auto r = std::from_chars(parts[1].data(), parts[1].data() + parts[1].size(), age);
    std::println("age next year = {}, ok = {}", age + 1, r.ec == std::errc{});

    try { int bad = std::stoi("twenty"); std::println("{}", bad); }
    catch (const std::invalid_argument&) { std::println("stoi failed: not a number"); }

    std::println("UTF-8 bytes in the Bangla word: {}", std::string("বাংলা").size());
}

getline(in, field, ',') প্রতিবার কমা পর্যন্ত পড়ে, কমাটি খেয়ে ফেলে; শেষ ঘরে কমা নেই, তাই stream-এর শেষ পর্যন্ত পড়ে। ফলে 3 fields: [Rafi] [21] [Dhaka]. এরপর from_chars-কে দেওয়া হলো লেখার শুরু আর শেষের ঠিকানা; সে age-এ 21 বসাল, আর r.ec খালি (std::errc{}) মানে সফল — তাই age next year = 22, ok = true. std::stoi("twenty") কোনো অঙ্ক পায়নি, তাই std::invalid_argument ছুড়েছে; catch সেটি ধরেছে, প্রোগ্রাম চলেছে। শেষ লাইনটি দেখায় 15 — পাঁচ “অক্ষরের” একটি বাংলা শব্দ, অথচ ১৫ byte. কেন, তা অংশ ৮-এ।

৬ · ভেতরের কথা — capacity, reallocation আর SSO

std::string কীভাবে “নিজেই বড় হয়”? ভেতরে সে তিনটি তথ্য রাখে: অক্ষরগুলোর মেমরির ঠিকানা, এখন কয়টি অক্ষর আছে (size()), আর নতুন মেমরি না চেয়ে মোট কয়টি রাখা যায় — capacity (capacity()). লম্বা লেখা থাকে heap-এ, অর্থাৎ প্রোগ্রাম চলার সময় চেয়ে নেওয়া মেমরিতে।

যতক্ষণ size() ≤ capacity(), += শুধু খালি জায়গায় অক্ষর বসায় — খুব সস্তা। জায়গা ফুরালে ঘটে reallocation: নতুন, সাধারণত প্রায় দ্বিগুণ মাপের একটি খণ্ড নেওয়া, পুরোনো অক্ষর সেখানে কপি করা, পুরোনো খণ্ড ফেরত দেওয়া। আর ছোট লেখার জন্য প্রায় সব লাইব্রেরি একটি বাড়তি কৌশল করে — Small String Optimization (SSO): লেখা ছোট হলে heap-এ না গিয়ে std::string object-এর নিজের ভেতরেই রেখে দেওয়া। ধাপে ধাপে দেখুন (সংখ্যাগুলো GCC-র লাইব্রেরি libstdc++-এর, 64-bit; অন্য লাইব্রেরিতে আলাদা হবে):

1std::string s;2s = "Dhaka";3s += "-Chattogram";4s += " Highway";5s += " N1 route";
string-এর ভেতরের অবস্থা
size()
capacity()
অক্ষর কোথায়
  1. খালি string-ও জায়গা নেয়। std::string s; heap থেকে কিছুই চায় না, কিন্তু object নিজে ৩২ byte (libstdc++-এ)। এর ভেতরে ঠিকানা, size আর ১৫টি অক্ষর রাখার মতো একটি ছোট ঘর আছে — তাই শুরুতেই capacity 15.
  2. ছোট লেখা — SSO. "Dhaka" ৫ অক্ষর, ভেতরের ঘরেই এঁটে যায়। কোনো heap allocation নেই, যা খুব দ্রুত। বাস্তব প্রোগ্রামের বহু লেখা — নাম, জেলা, কোড — এমন ছোট, তাই এই কৌশলের লাভ বিশাল।
  3. জায়গা ফুরাল — প্রথম reallocation. ১৬ অক্ষর ১৫-এর ঘরে ধরে না। string heap থেকে ৩০ অক্ষরের খণ্ড A নিল (পুরোনো capacity-র দ্বিগুণ), পুরোনো ৫ অক্ষর সেখানে কপি করল, তারপর নতুন অক্ষর বসাল। শেষে NUL-এর জন্য এক byte বাড়তি সবসময় থাকে।
  4. জায়গা আছে — সস্তা যোগ। ২৪ ≤ ৩০, তাই নতুন ৮টি অক্ষর খালি জায়গায় বসল। কোনো কপি নেই, কোনো নতুন মেমরি নেই। প্রায় দ্বিগুণ করে বাড়ার কারণেই অনেকবার += করলেও গড়ে প্রতিটি যোগ সস্তা থাকে।
  5. আবার reallocation — আর পুরোনো ঠিকানা মৃত। ৩৩ > ৩০, তাই ৬০-এর খণ্ড B, ২৪ অক্ষর কপি, খণ্ড A ফেরত। কেউ যদি খণ্ড A-এর দিকে একটি pointer বা string_view ধরে রাখে, সেটি এখন একটি ফেরত দেওয়া মেমরির দিকে তাকিয়ে আছে। একে বলে invalidation. আগে থেকে মোট আকার জানলে s.reserve(100) দিয়ে একবারেই জায়গা নিয়ে রাখা যায়।

SSO standard-এর নিয়ম নয়, লাইব্রেরির কৌশল। C++ standard শুধু বলে std::string কী করবে, ভেতরে কীভাবে করবে তা বলে না। তাই ঘরের মাপ আলাদা: GCC-র libstdc++ আর Microsoft-এর লাইব্রেরিতে ১৫ অক্ষর (object ৩২ byte), আর Clang-এর libc++-এ ২২ অক্ষর, যেখানে object ২৪ byte-এর। কোডে কখনো এই সংখ্যাগুলোর ওপর নির্ভর করবেন না — শুধু অনুভূতিটা রাখুন: ছোট লেখা সস্তা, বড় লেখা heap-এ যায়, আর বাড়ার সময় ঠিকানা বদলাতে পারে।

৭ · std::string_view — শুধু দেখার জানালা

পর্ব ১০-এ শিখেছেন, শুধু-পড়ার লেখা পাঠাতে const std::string&. কিন্তু এর একটি লুকানো খরচ আছে। ফাংশনটিকে যদি "Rahim"-এর মতো string literal দেন, compiler আগে একটি অস্থায়ী std::string বানায় — লেখা লম্বা হলে heap-এ মেমরি চেয়ে, অক্ষর কপি করে — শুধু reference দেওয়ার জন্য। C++17 এর সমাধান দিয়েছে std::string_view — header <string_view>-এ।

একটি string_view-এর ভেতরে শুধু দুটি সংখ্যা — লেখার শুরুর ঠিকানা আর দৈর্ঘ্য; 64-bit সিস্টেমে মোট ১৬ byte. এটি std::string, string literal বা অন্য কোনো char-এর সারির দিকে তাকাতে পারে, কোনো কপি ছাড়াই। আর এর substr নতুন লেখা বানায় না, শুধু ঠিকানা আর দৈর্ঘ্য বদলে নতুন একটি view দেয়। find, size, starts_with, তুলনা — শুধু পড়ার প্রায় সব ফাংশনই আছে। নেই শুধু বদলানোর ফাংশন: +=, insert, replace.

// শুধু পড়ে, তাই string_view: std::string, literal — সবই কপি ছাড়া চলে
std::size_t countDigits(std::string_view text) {
    std::size_t n = 0;
    for (char c : text) if (c >= '0' && c <= '9') ++n;
    return n;
}
countDigits("Road 12, House 7");      // কোনো std::string তৈরি হয় না
countDigits(addressString);           // std::string থেকেও সরাসরি

কিন্তু “শুধু দেখা” মানে মালিকানা নেই — আর মালিকানা না থাকলে মালিকের আয়ুর ওপর পুরো নির্ভরতা। এটাই string_view-এর বিপদ, আর পর্ব ১০-এর dangling reference-এর মতোই এটি compile হয়, অনেক সময় চলেও। নিচের কোডে একটি নিরাপদ ব্যবহার আর দুটি ভুল আছে। ধাপে ধাপে দেখুন:

1std::string city = "Dhaka-1205";2std::string_view v = city;3std::string_view code = v.substr(6);4std::string_view bad = std::string("Sylhet") + "-3100";5std::println("{}", bad);         // undefined behavior6city += " Bangladesh";           // v আর code অবৈধ
view আর তার মালিক
city
v
code
bad
  1. মালিক তৈরি। city একটি std::string, ১০ অক্ষর — SSO-র কারণে object-এর ভেতরের ঘরেই আছে। অক্ষরগুলোর মালিক সে।
  2. নিরাপদ view. v শুধু city-র অক্ষরের শুরুর ঠিকানা আর দৈর্ঘ্য ১০ মনে রাখল। কোনো অক্ষর কপি হয়নি। city যতক্ষণ বেঁচে আছে আর বদলায়নি, v সম্পূর্ণ নিরাপদ।
  3. View-এর substr — তবুও কপি নেই। v.substr(6) নতুন লেখা বানায় না; একই অক্ষরের দিকে, ৬ ঘর এগিয়ে, দৈর্ঘ্য ৪ — "1205". বিশাল লেখা টুকরো টুকরো পড়তে এটাই string_view-এর সবচেয়ে বড় সুবিধা।
  4. ভুল ১ — temporary-র দিকে view. ডান পাশের std::string("Sylhet") + "-3100" একটি নাম-ছাড়া temporary std::string. bad তার অক্ষরের দিকে তাকাল, তারপর লাইনের শেষে (;) temporary-টি ধ্বংস হলো। পর্ব ১০-এর const reference-এর মতো আয়ু বাড়ানোর কোনো নিয়ম এখানে খাটে না — string_view একটি আলাদা object, reference নয়।
  5. Dangling view পড়া। bad এখন একটি dangling view — ঠিকানা আছে, মালিক নেই। পড়া মানে undefined behavior. সবচেয়ে বিপজ্জনক ফল হলো ঠিকঠাক Sylhet-3100 দেখানো, যাতে আপনি ভাবেন সব ঠিক। সমাধান: এখানে std::string bad = …; — মালিক হয়ে রাখুন।
  6. ভুল ২ — মালিক বদলে গেল। মালিক বেঁচে থাকলেও যথেষ্ট নয়। city += " Bangladesh"-এ লেখা ২১ অক্ষরের হলো, ভেতরের ঘরে ধরল না, তাই অক্ষরগুলো heap-এর নতুন ঠিকানায় সরে গেল — অংশ ৬-এর সেই reallocation-ই। v আর code এখনো পুরোনো ঠিকানা দেখছে — তারা অবৈধ। নিয়ম: view থাকা অবস্থায় মালিককে বদলাবেন না।

কখন string_view, কখন নয়

পরিস্থিতিসিদ্ধান্তকারণ
ফাংশন লেখা শুধু পড়বে, এখনই কাজ শেষ করবেstd::string_view by valueসবচেয়ে প্রচলিত আর নিরাপদ ব্যবহার; ডাকার মানুষের লেখা ফাংশন শেষ হওয়া পর্যন্ত বাঁচে
ফাংশন লেখাটি রেখে দেবে (struct-এর member-এ, vector-এ)std::stringরেখে দিতে মালিকানা লাগে; view রাখলে পরে মালিক মরে যেতে পারে
ফাংশন থেকে লেখা ফেরত দেওয়াসাধারণত std::stringনিজে বানানো লেখার view ফেরত দিলে নিশ্চিত dangling
C ফাংশনকে দেওয়া (fopen, printf("%s"))std::string আর .c_str()string_view-এর শেষে '\0' থাকার নিশ্চয়তা নেই — v.data() C-কে দিলে সে সীমা পেরিয়ে পড়তে পারে

View-এর আরেকটি ফাঁদ হলো std::string_view sv = getName();, যেখানে getName() একটি std::string by value ফেরত দেয় — এটিও temporary-র দিকে view. Clang-এর মতো compiler প্রায়ই এখানে সতর্কবার্তা দেয়, কিন্তু সব ক্ষেত্রে ধরতে পারে না। কোন object কতক্ষণ বাঁচে, পুরো ছবিটি পর্ব ১৮-এ “lifetime” নামে দেখবেন; আর ঠিকানা জিনিসটা আসলে কী, তা পরের পর্বে pointer দিয়ে।

৮ · বাংলা লেখা — byte, code point আর অক্ষর

এ পর্যন্ত সব উদাহরণ ইংরেজিতে ছিল, যেখানে একটি char = একটি অক্ষর। বাংলায় এই সরল হিসাব ভেঙে যায়। কারণ std::string আসলে অক্ষর চেনে না — সে শুধু byte-এর একটি সারি রাখে। সেই byte-গুলোর অর্থ কী, তা ঠিক করে একটি encoding, আর আজকের প্রায় সব বাংলা লেখা থাকে UTF-8-এ।

প্রথমে Unicode: এটি পৃথিবীর প্রায় সব লিপির প্রতিটি চিহ্নকে একটি নম্বর দেয়, যাকে বলে code point. বাংলার নম্বরগুলো U+0980 থেকে U+09FF-এর মধ্যে। UTF-8 সেই নম্বরকে ১ থেকে ৪ byte-এ লেখে: ইংরেজি বর্ণ (ASCII) ১ byte-এ, আর বাংলার প্রতিটি code point ৩ byte-এ। "বাংলা" শব্দটিকে byte ধরে ধরে খুলে দেখুন:

std::string bn = "বাংলা";চোখে যা দেখেন
লেখা:        বাংলা
code point:  ব      া      ং      ল      া
নম্বর:       U+09AC U+09BE U+0982 U+09B2 U+09BE
             ৫টি code point  (কার-চিহ্নও আলাদা code point)
bn.size()std::string যা দেখে
index:  0  1  2 | 3  4  5 | 6  7  8 | 9 10 11 |12 13 14
byte:  E0 A6 AC |E0 A6 BE |E0 A6 82 |E0 A6 B2 |E0 A6 BE
        ব       |   া     |   ং     |   ল     |   া
bn.size() == 15   <- byte, অক্ষর নয়
bn[0]একটি byte মাত্র
bn[0] == 0xE0     (x86-এ char signed, তাই মান -32)
এটি "ব" নয় — "ব"-এর ৩ byte-এর প্রথমটি
একা ছাপলে terminal দেখায় অচেনা চিহ্ন: �
bn.substr(0, 4)অক্ষরের মাঝখানে কাটা
byte:  E0 A6 AC | E0 A6 BE ...
        ব       | ভাঙা
ফল: "ব" + অসম্পূর্ণ একটি byte -> ভুল UTF-8
bn.substr(0, 3) == "ব"   <- byte-এর হিসাবে কাটলে ঠিক
code point গোনাপ্রথম byte গুনুন
প্রথম byte: E0 (1110xxxx)   পরের byte: A6, AC (10xxxxxx)
for (unsigned char b : bn)
    if ((b & 0xC0) != 0x80) ++count;   // "10xxxxxx" বাদ
count == 5   (কিন্তু পাঠক দেখেন বাং + লা)
  1. চোখের অক্ষর বনাম code point. আপনি দেখেন একটি শব্দ, কিন্তু Unicode-এর হিসাবে এটি ৫টি code point: ব, া-কার, ং (অনুস্বার), ল, া-কার। কার-চিহ্নগুলো ব্যঞ্জনের সাথে জোড়া লেগে দেখায়, তবু তারা আলাদা নম্বর।
  2. size() গোনে byte. প্রতিটি বাংলা code point UTF-8-এ ৩ byte, তাই ৫ × ৩ = ১৫. bn.size() সত্যিই 15 দেয় — অংশ ৫-এর কোড চালালে নিজেই দেখেছেন। “নাম সর্বোচ্চ ২০ অক্ষর” নিয়ম যদি size() <= 20 দিয়ে লেখেন, একটি বাংলা নাম মাত্র ছয়-সাত অক্ষরেই আটকে যাবে।
  3. bn[0] একটি byte, অক্ষর নয়। bn[0] হলো 0xE0 — “ব”-এর তিন byte-এর প্রথমটি। x86 মেশিনে char সাধারণত signed, তাই সংখ্যা হিসেবে -32. একে আলাদা করে ছাপলে terminal বোঝে না, অচেনা চিহ্ন দেখায়।
  4. byte-এর হিসাবে না কাটলে লেখা ভাঙে। substr(0, 4) “ব”-এর তিন byte আর “া”-এর প্রথম byte নিল — শেষ টুকরোটি অসম্পূর্ণ, ফলে ভুল UTF-8. find আর substr বাংলাতেও কাজ করে, যদি অবস্থানগুলো find-এর ফল থেকে আসে (যেমন find("লা")-এর ফল 9, যেখান থেকে “ল”-এর byte শুরু)। হাতে গোনা “৩ নম্বর অক্ষর” থেকে নয়।
  5. Code point গোনা যায়, “অক্ষর” গোনা কঠিন। UTF-8-এ প্রতিটি code point-এর পরের byte-গুলো 10xxxxxx ধাঁচের। সেগুলো বাদ দিয়ে গুনলে পাওয়া যায় ৫. কিন্তু পাঠকের চোখে অক্ষর হয়তো দুটি গুচ্ছ — “বাং” আর “লা”। এমন গুচ্ছকে বলে grapheme cluster, আর এর নিয়ম এতই জটিল যে C++ standard library-তে এটি গোনার কোনো ফাংশন নেই। দরকার হলে ICU-এর মতো বিশেষ লাইব্রেরি লাগে।

char8_t, u8 আর বড় হাত-ছোট হাত

C++20 একটি আলাদা টাইপ এনেছে, char8_t, যার অর্থ “এই byte UTF-8 লেখার অংশ”। u8"বাংলা" literal-এর টাইপ C++20 থেকে const char8_t-এর array, আর এর জন্য আছে std::u8string. উদ্দেশ্য ভালো — টাইপ দেখেই encoding বোঝা। কিন্তু বাস্তবে std::cout, std::format বা বেশিরভাগ লাইব্রেরি এখনো সাধারণ char চায়, তাই অনেক দল সাধারণ std::string-এই UTF-8 রাখে আর ফাইল UTF-8-এ সেভ করে। আপনিও এখন সেটাই করুন; terminal-এ বাংলা দেখানোর ঝামেলা পর্ব ০৭-এর শেষ অংশে দেখেছেন।

আরেকটি সৎ সতর্কতা: <cctype>-এর std::toupper আর std::tolower শুধু একটি byte দেখে, তাই এগুলো শুধু ASCII ইংরেজি বর্ণে নির্ভরযোগ্য। বাংলায় বড় হাত-ছোট হাত নেই, কিন্তু তুর্কি বা জার্মানের মতো ভাষায় এক byte ধরে বদলানো ভুল ফল দেয়। আর ব্যবহারের সময় char-কে আগে unsigned char-এ বদলে নিন — std::toupper(static_cast<unsigned char>(c)) — কারণ ঋণাত্মক মান (যেমন বাংলার byte 0xE0 = -32) দিলে ফল undefined behavior.

পরীক্ষা ও ইন্টারভিউতে
  • std::string আর C string-এর মূল পার্থক্য? — std::string নিজে মেমরি সামলায়, দৈর্ঘ্য জমা রাখে, দরকারে বড় হয় আর নিরাপদে কপি হয়; C string হলো NUL-এ শেষ হওয়া char array, সব দায়িত্ব প্রোগ্রামারের।
  • find কিছু না পেলে কী দেয়? — std::string::npos, যা size_t-এর সবচেয়ে বড় মান; তুলনা সবসময় != npos দিয়ে।
  • s[i] আর s.at(i)-এর পার্থক্য? — at সীমা পরীক্ষা করে std::out_of_range ছোড়ে; [] পরীক্ষা করে না, সীমার বাইরে undefined behavior.
  • string_view কেন বিপজ্জনক হতে পারে? — এটি মালিক নয়; temporary বা বদলে যাওয়া string-এর দিকে তাকালে dangling হয়, আর শেষে NUL থাকার নিশ্চয়তা নেই।
  • UTF-8-এ std::string("বাংলা").size() কত? — 15: ৫টি code point, প্রতিটি ৩ byte.

৯ · AI-এর যুগে — লেখা নিয়ে AI-এর চেনা ভুল

লেখা-সংক্রান্ত কোড AI খুব দ্রুত লিখে দেয়, কিন্তু তার শেখার উৎসে প্রচুর পুরোনো C-ধাঁচের কোড আর ইংরেজি-কেন্দ্রিক উদাহরণ আছে। ফলে ভুলগুলো প্রায় একই রকম ফিরে আসে — আর বেশিরভাগই ছোট ইংরেজি ইনপুটে ধরা পড়ে না। বাংলা নাম, ফাঁকা ঘর বা লম্বা ইনপুট দিলেই ভাঙে।

AI যা লেখেসমস্যাযা হওয়া উচিত
char name[50]; strcpy(name, input.c_str());C++-এ অপ্রয়োজনীয় C string; বড় ইনপুটে buffer overflowstd::string name = input;
std::string_view first = line.substr(0, 5);std::string::substr temporary দেয়, view সাথে সাথে danglingstd::string first = …, বা std::string_view(line).substr(0, 5)
ফাংশন থেকে local string-এর string_view ফেরতফাংশন শেষে মালিক মরে, view danglingstd::string by value ফেরত
if (name.size() > 20) — “নাম ২০ অক্ষরের বেশি নয়”বাংলায় size() byte গোনে; ৭ অক্ষরের নামও আটকে যায়নিয়মটি byte-এ লিখুন, বা code point গুনুন — কোনটি চান, স্পষ্ট করুন
int age = std::stoi(field); — try/catch ছাড়াফাঁকা বা ভুল ঘরে exception, প্রোগ্রাম বন্ধtry/catch, বা std::from_chars আর ec যাচাই
int pos = s.find(','); if (pos) …npos int-এ ঢুকে অর্থ হারায়; অবস্থান 0-তে পেলেও শর্ত মিথ্যাauto pos = …; if (pos != std::string::npos)

AI-কে কাজ দেওয়ার সময় মালিকানা, ইনপুটের ধরন আর ভুল সামলানোর নিয়ম আগেই বলে দিন, আর পরীক্ষার ইনপুটও চেয়ে নিন:

আমি C++23 শিখছি (g++, -Wall -Wextra). একটি ফাংশন লেখো যা "Rafi,21,Dhaka" ধরনের একটি CSV লাইন পড়ে name, age আর city আলাদা করবে। নিয়ম: ১. char array, strcpy, strtok বা sprintf ব্যবহার করবে না; std::string আর std::string_view ব্যবহার করবে। ২. ফাংশনের প্যারামিটার std::string_view হবে, কিন্তু ফেরত দেওয়া বা struct-এ রাখা সব লেখা std::string হবে — কোনো string_view যেন temporary বা local string-এর দিকে না তাকায়। ৩. find-এর প্রতিটি ফল std::string::npos-এর সাথে মিলিয়ে দেখবে। ৪. age রূপান্তরে std::from_chars ব্যবহার করবে আর error code যাচাই করবে; ভুল হলে std::optional বা স্পষ্ট error ফেরত দেবে, প্রোগ্রাম crash করবে না। ৫. নাম বাংলায় (UTF-8) থাকতে পারে, তাই size()-কে অক্ষর-সংখ্যা ধরে নেবে না। কোডের পরে ৫টি পরীক্ষার ইনপুট দাও — ফাঁকা ঘর, কমা কম, age-এ অক্ষর, বাংলা নাম, অনেক লম্বা লাইন — আর প্রতিটিতে কী হবে, সহজ বাংলায় বলো।

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

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

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

প্র ০১C++ ১৯৮৫ সালে বাজারে এলেও std::string standard হলো ১৯৯৮ সালে। এত দেরি কেন, আর C++11 কেন copy-on-write string কার্যত নিষিদ্ধ করল?

C++-এর প্রথম দশকে কোনো standard library string ছিল না। ফলে প্রতিটি বড় লাইব্রেরি নিজের string বানিয়েছিল — Microsoft-এর MFC-তে CString, Qt-তে QString, আরও বহু কোম্পানির নিজস্ব class. একটি প্রোগ্রামে তিনটি লাইব্রেরি মানে তিন রকম string আর তাদের মধ্যে অবিরাম রূপান্তর। প্রথম ISO C++ standard (C++98) অবশেষে std::basic_string আনল, যার char রূপই std::string.

দেরির একটি বড় কারণ ছিল নকশার টানাপোড়েন। string-এর নকশা প্রায় তৈরি, এমন সময় (১৯৯৪) Alexander Stepanov-এর STL standard-এ গৃহীত হয়, আর string-কে container-এর মতো iterator-ওয়ালা করে নতুন করে সাজাতে হয়। পুরোনো অবস্থান-ভিত্তিক ফাংশন (find, substr) আর নতুন iterator-ভিত্তিক ফাংশন দুটোই রয়ে গেল। Herb Sutter পরে তাঁর “Guru of the Week” সিরিজে দেখিয়েছিলেন, std::string-এ একশোর বেশি member function, যার অনেকগুলো সাধারণ algorithm দিয়েই করা যেত — “monolith” নকশার পাঠ্যপুস্তকের উদাহরণ।

দ্বিতীয় গল্পটি copy-on-write (COW) কৌশলের। ধারণা: কপি করার সময় আসলে অক্ষর কপি না করে দুটি string একই মেমরি ভাগ করবে, সাথে একটি গণনা (“কতজন ব্যবহার করছে”)। কেউ বদলাতে গেলে তখনই আলাদা কপি। ১৯৯০-এর দশকে কপি দামি ছিল, তাই GCC-র libstdc++-সহ অনেক লাইব্রেরি এটি করত। সমস্যা দেখা দিল multithreading-এ: প্রতিটি কপি আর ধ্বংসে সেই গণনা atomic ভাবে বাড়াতে-কমাতে হয়, যা একাধিক CPU core-এ ধীর। আরও সূক্ষ্ম সমস্যা — non-const s[i] ডাকলেই লাইব্রেরিকে ধরে নিতে হতো আপনি বদলাবেন, তাই আগাম কপি, আর আগে নেওয়া reference হঠাৎ অবৈধ। Sutter ১৯৯৯ সালেই “Optimizations That Aren't (In a Multithreaded World)” লেখায় এ নিয়ে সতর্ক করেছিলেন।

C++11 প্রথমবার ভাষায় thread-এর নিয়ম আনল, আর string-এর জন্য এমন শর্ত দিল (যেমন operator[] বা begin() ডাকলে reference অবৈধ হবে না) যা COW-এর সাথে মেলে না। ফলে GCC ২০১৫ সালে GCC 5-এ নতুন SSO-ভিত্তিক string আনে। পুরোনো আর নতুন compile করা কোড একসাথে link করতে দুটি ABI রাখতে হয়েছিল — Linux জগতে বহু বছর এর ঝামেলা চলেছে।

মূল উপলব্ধি: একটি “স্বাভাবিক” টাইপের নকশাও যুগের হার্ডওয়্যারের সাথে বদলায়। একসময়ের চতুর optimization (COW) multicore যুগে বোঝা হয়ে দাঁড়ায়, আর standard-এর একটি বাক্য পুরো ecosystem-এর ABI ভেঙে দিতে পারে।

প্র ০২SSO যদি এত ভালো হয়, তাহলে ভেতরের ঘরটা আরও বড় — ধরুন ১০০ অক্ষরের — করা হয় না কেন? এই কৌশলের দাম কী?

SSO-র লাভ স্পষ্ট: heap allocation বেশ দামি কাজ — allocator-কে খালি জায়গা খুঁজতে হয়, multithreaded প্রোগ্রামে কখনো lock নিতে হয়, আর পরে ফেরতও দিতে হয়। বাস্তব প্রোগ্রামে বিপুল পরিমাণ string ছোট: নাম, শহর, JSON-এর key, HTTP header. এগুলো heap-এ না গেলে প্রোগ্রাম দ্রুত হয়, আর ডেটা পাশাপাশি থাকায় CPU cache-ও ভালো কাজ করে।

কিন্তু ঘরটা থাকে প্রতিটি std::string object-এর ভেতরে — লেখা ছোট হোক বা বড়, খালি হোক বা ভরা। ঘর বড় করলে প্রতিটি object বড় হয়। এক কোটি উপাদানের std::vector<std::string>-এ প্রতিটি object ৩২ থেকে বেড়ে ১২০ byte হলে, শুধু object-এর জন্যই প্রায় ৯০ কোটি বাড়তি byte — যার বেশিরভাগ খালি পড়ে থাকবে। আবার লম্বা লেখার ক্ষেত্রে ঘরটি পুরোই অপচয়, কারণ তখন অক্ষর থাকে heap-এ।

দ্বিতীয় দাম move-এ। দুটি heap-ভিত্তিক string-এর মধ্যে move মানে শুধু ঠিকানা হাতবদল (পর্ব ৩৬)। কিন্তু ছোট string-এর অক্ষর object-এর ভেতরে, তাই move করতেও আসলে byte কপি করতে হয়। ঘর বড় হলে move ধীর হয়। তৃতীয় দাম জটিলতা: প্রতিটি কাজে লাইব্রেরিকে পরীক্ষা করতে হয় “এখন ছোট, না বড় অবস্থা?” — একটি বাড়তি শাখা।

তাই লাইব্রেরিগুলো ভিন্ন ভিন্ন আপস করেছে। libstdc++ (GCC) আর Microsoft-এর লাইব্রেরি ৩২ byte-এর object-এ ১৫ অক্ষর রাখে। libc++ (Clang) আরও চতুর: ২৪ byte-এর object-এ ২২ অক্ষর, কারণ ছোট অবস্থায় ঠিকানা, size আর capacity-র ঘরগুলোই অক্ষর রাখার কাজে লাগে, আর একটি bit দিয়ে বোঝানো হয় কোন অবস্থা চলছে। Facebook-এর folly লাইব্রেরির fbstring ২৪ byte-এ ২৩ অক্ষর রাখে — শেষ byte-এ “আর কত জায়গা বাকি” লেখা থাকে, যা ঘর পুরো ভরলে শূন্য হয়ে নিজেই NUL-এর কাজ করে।

সঠিক মাপ নির্ভর করে কাজের ধরনের ওপর, আর সেটা জানার একমাত্র উপায় মাপা। এ কারণেই কিছু বড় প্রজেক্ট (যেমন LLVM-এর SmallString<N>) মাপটি template parameter হিসেবে প্রোগ্রামারের হাতে ছেড়ে দেয়।

মূল উপলব্ধি: প্রতিটি optimization কোনো একটি সাধারণ ক্ষেত্রকে দ্রুত করে আর অন্য কোনো ক্ষেত্রের দাম বাড়ায়। “দ্রুত” কথাটির অর্থ সবসময় “কোন ডেটায়, কোন কাজে দ্রুত?” — আর এর উত্তর আসে মেপে, অনুমানে নয় (পর্ব ৪৯)।

প্র ০৩string_view এত সহজে dangling হয়, তবু C++17 এটি আনল কেন? আর অন্য ভাষাগুলো এই সমস্যা কীভাবে সামলায়?

ধারণাটি নতুন ছিল না। বড় কোম্পানিগুলো বহু বছর ধরে নিজস্ব “string-এর দিকে তাকানো” টাইপ ব্যবহার করছিল — Google-এর StringPiece, LLVM-এর StringRef, Boost-এর string_ref. কারণ একই: বিশাল লেখা (ওয়েব পেজ, সোর্স কোড, log ফাইল) টুকরো টুকরো বিশ্লেষণ করতে প্রতিটি টুকরোর জন্য নতুন std::string বানানো মানে লাখো অপ্রয়োজনীয় allocation. ২০১২ সালের দিকের প্রস্তাবের পর ধারণাটি Library Fundamentals TS হয়ে C++17-এ std::string_view নামে এল। আরেকটি সুবিধা: একই ফাংশন এখন std::string, literal, বা অন্য লাইব্রেরির লেখা — সবই নিতে পারে।

কিন্তু C++ এখানে একটি পরিচিত দাম মেনে নিয়েছে: view নিরাপদ কি না, তা compiler প্রমাণ করে না, প্রোগ্রামারকে মনে রাখতে হয়। বিখ্যাত ফাঁদগুলো: temporary-র view (std::string_view v = a + b;); std::string::substr-এর ফলের view, যা নিজেই temporary; ফাংশন থেকে local string-এর view ফেরত; struct-এর member হিসেবে view রেখে মালিককে আগে মরতে দেওয়া; আর view রেখে মালিককে বড় করা। আরও একটি সূক্ষ্ম ফাঁদ — string_view-এর শেষে NUL নেই, তাই printf("%s", v.data()) বা C API-তে data() দেওয়া ভুল।

এ কারণে অনেক অভিজ্ঞ প্রোগ্রামার নিয়ম মানেন: string_view শুধু ফাংশনের প্যারামিটার আর লুপের ভেতরের ক্ষণস্থায়ী ভ্যারিয়েবলে; struct-এর member বা ফেরত টাইপে শুধু তখন, যখন মালিকের আয়ু স্পষ্টভাবে নিশ্চিত। Compiler-ও চেষ্টা করছে — Clang-এর lifetime বিশ্লেষণ অনেক সাধারণ ক্ষেত্রে সতর্ক করে, আর AddressSanitizer চলার সময় ধরে ফেলে। কিন্তু কোনোটিই সব ক্ষেত্রে নিশ্চয়তা দেয় না।

অন্য ভাষার পথ আলাদা। Java, Python, C#-এ string immutable আর garbage collector-এর পাহারায়, তাই কোনো view মালিকের আগে মরতে পারে না — দাম হলো GC আর অনেক সময় অপ্রয়োজনীয় কপি। Rust-এর &str হুবহু string_view-এর মতো (ঠিকানা আর দৈর্ঘ্য), কিন্তু borrow checker compile-এর সময়েই প্রমাণ করে view মালিকের চেয়ে বেশি দিন বাঁচবে না, আর view থাকা অবস্থায় মালিককে বদলাতেও দেয় না। উপরের stepper-এর দুটি ভুলই Rust-এ compile হয় না।

মূল উপলব্ধি: string_view “কপি ছাড়া দেখার” গতি দেয়, আর বিনিময়ে আয়ুর হিসাব রাখার দায়িত্ব আপনাকে দেয়। এটি C++-এর মূল দর্শন — শূন্য খরচের হাতিয়ার, তবে নিরাপত্তার দায় প্রোগ্রামারের — আর এ কারণেই পর্ব ১৮-এর lifetime-এর ধারণা এত গুরুত্বপূর্ণ।

প্র ০৪Python-এ len("বাংলা") দেয় 5, C++-এ size() দেয় 15. C++-এ কেন এখনো একটি পূর্ণ Unicode text লাইব্রেরি নেই?

প্রথমে বুঝুন, “লেখার দৈর্ঘ্য” প্রশ্নটির অন্তত চারটি সঠিক উত্তর আছে: byte (UTF-8-এ ১৫), UTF-16 code unit (৫ — Java-র length() এটাই গোনে), code point (৫ — Python-এর len), আর grapheme cluster (পাঠকের চোখের অক্ষর — Swift-এর String.count এটাই গোনে)। ইমোজি বা যুক্তাক্ষরে এই চারটি সংখ্যা একেবারে আলাদা হয়। ভাষাগুলো ভিন্ন উত্তর বেছেছে, কারণ কোনোটিই সব কাজের জন্য সঠিক নয় — ফাইলের আকারে byte লাগে, স্ক্রিনে কার্সর সরাতে grapheme.

C++ ঐতিহাসিকভাবে C-এর পথ নিয়েছিল: char মানে একটি byte, আর encoding-এর অর্থ নির্ভর করে “locale”-এর ওপর — প্রতিটি কম্পিউটারের সেটিং। Windows-এ wchar_t ১৬ bit (UTF-16), Linux-এ ৩২ bit — ফলে std::wstring-ও portable হয়নি। C++11 char16_t, char32_t আর রূপান্তরের জন্য std::wstring_convert আনে, কিন্তু সেগুলো এত জটিল আর ত্রুটিপূর্ণ ছিল যে C++17-এই deprecated হয়ে যায়।

পূর্ণ Unicode সমর্থন সত্যিই বিশাল কাজ: normalization (একই অক্ষর একাধিক উপায়ে লেখা যায়), grapheme ভাগ, বড়-ছোট হাতের ভাষাভিত্তিক নিয়ম, ভাষা অনুযায়ী সাজানো, যাকে বলে collation. এর ডেটা-টেবিল Unicode-এর প্রতিটি নতুন সংস্করণে বদলায়, অথচ C++ standard আসে তিন বছরে একবার আর বহু বছর টিকে থাকতে হয়। তাই কমিটি সতর্ক। এই কাজের জন্য ২০১৮ সালের দিকে একটি আলাদা দল (SG16, Unicode study group) গঠিত হয়েছে; তাদের কাজের ফল char8_t (C++20), আর std::format/std::print-এ Unicode-এর ভালো সমর্থন — যেমন Windows console-এ UTF-8 ঠিকভাবে পাঠানো।

বাস্তবে আজ যা করা হয়: ভেতরে সব লেখা UTF-8 std::string-এ রাখা (“UTF-8 everywhere” নীতি), byte-এর হিসাবে find/substr করা, আর সত্যিকারের ভাষাগত কাজ — grapheme গোনা, সাজানো, বড়-ছোট হাত — ICU-এর মতো প্রতিষ্ঠিত লাইব্রেরিকে দেওয়া। ব্রাউজার আর অপারেটিং সিস্টেমের মতো বড় সফটওয়্যারও এভাবেই চলে।

মূল উপলব্ধি: “অক্ষর” কোনো সরল ধারণা নয় — byte, code point আর পাঠকের চোখের অক্ষর আলাদা স্তর। C++ শুধু সবচেয়ে নিচের স্তর (byte) নিশ্চিতভাবে দেয়, আর ওপরের স্তরের জন্য আপনাকে সচেতনভাবে লাইব্রেরি বেছে নিতে হয়। বাংলা নিয়ে কাজ করা প্রোগ্রামারের জন্য এই সচেতনতা ঐচ্ছিক নয়।

প্র ০৫C-এর printf/sprintf দিয়ে লেখা বানানো কেন নিরাপত্তার ঝুঁকি, আর std::format ও std::string কীভাবে সেটা কমায়?

C-এর লেখা-সংক্রান্ত ঝুঁকি দুই ধরনের। প্রথমটি মাপের ভুল: sprintf(buf, "Name: %s", name) জানে না buf কত বড়। name লম্বা হলে buffer overflow. strcpy, strcat, gets — সবারই একই রোগ। আক্রমণকারী ইচ্ছামতো লম্বা ইনপুট পাঠিয়ে stack-এর ফেরত-ঠিকানা পর্যন্ত বদলে দিতে পারে, যা কয়েক দশক ধরে হ্যাকিংয়ের সবচেয়ে প্রচলিত কৌশলগুলোর একটি ছিল।

দ্বিতীয়টি format string-এর ভুল। printf(userInput) — ইনপুটটাকেই format হিসেবে দেওয়া — দেখতে নিরীহ। কিন্তু ব্যবহারকারী যদি %x %x %x লেখেন, printf stack থেকে এমন মান পড়তে থাকে যা কেউ পাঠায়নি — গোপন তথ্য ফাঁস। আর %n নির্দেশটি এখন পর্যন্ত ছাপা অক্ষরের সংখ্যা মেমরিতে লিখে দেয়, তাই আক্রমণকারী মেমরি বদলাতেও পারেন। ১৯৯৯-২০০০ সালের দিকে এই “format string attack” ব্যাপকভাবে পরিচিত হয়, আর বহু সার্ভার প্রোগ্রামে এর গর্ত পাওয়া যায়। এখন compiler -Wformat-security দিয়ে সতর্ক করে, তবু সমস্যার মূল রয়ে গেছে — printf চলার সময় টাইপ বা আর্গুমেন্টের সংখ্যা যাচাই করতে পারে না।

আধুনিক C++ দুই স্তরে এর সমাধান দেয়। std::string মাপের সমস্যা দূর করে — += বা std::format-এর ফল যত লম্বাই হোক, string নিজেই জায়গা নেয়। আর std::format (C++20, Victor Zverovich-এর {fmt} লাইব্রেরি থেকে) format string-কে compile-এর সময়েই পরীক্ষা করে: {}-এর সংখ্যা আর আর্গুমেন্টের টাইপ না মিললে compile error, প্রোগ্রাম চলার পর গোপন ভুল নয়। এই compile-time যাচাই একটি পরবর্তী সংশোধনের মাধ্যমে C++20-এর ওপরেই প্রয়োগ করা হয়েছে। এতে %n-এর মতো মেমরিতে লেখার কোনো নির্দেশও নেই। ব্যবহারকারীর লেখাকে format হিসেবে দেওয়া সাধারণ std::format(userInput)-এ compile-ই হয় না; ইচ্ছা করে করতে হলে আলাদা std::vformat লাগে।

তবে কোনো টাইপ সব ঝুঁকি দূর করে না। std::string ব্যবহার করেও s[i] সীমার বাইরে পড়া, dangling string_view, বা .c_str()-কে পুরোনো C ফাংশনে দেওয়ার পর সেই ফাংশনের ভুল — সবই সম্ভব। পর্ব ৪৩-এ undefined behavior আর নিরাপদ C++-এর বড় ছবিটি দেখবেন।

মূল উপলব্ধি: নিরাপত্তা আসে নকশা থেকে — যে ভুল টাইপ-ব্যবস্থাই হতে দেয় না, সেটি কোনো ক্লান্ত প্রোগ্রামার রাত দুটোয় করতে পারেন না। C-এর “প্রোগ্রামার সব জানেন” দর্শনের বদলে আধুনিক C++ চেষ্টা করে ভুলকে compile error বানাতে।

অনুশীলন

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

  1. আউটপুট অনুমান: std::string s = "Bangladesh"; — এরপর (ক) s.find("desh") (খ) s.substr(0, 6) (গ) s.replace(0, 6, "BD") করার পর s আর s.size() (ঘ) s.find('x') == std::string::npos — প্রতিটির ফল কী?

    (ক) 6 — B=0, a=1, n=2, g=3, l=4, a=5, d=6. (খ) "Bangla" — অবস্থান 0 থেকে ৬টি অক্ষর। (গ) প্রথম ৬টি অক্ষর (“Bangla”) বদলে “BD” বসে: "BDdesh", আর size() 6. (ঘ) true — লেখায় x নেই, তাই find দেয় npos.

  2. টাইপ বেছে নিন: প্রতিটি ক্ষেত্রে std::string না std::string_view, কারণসহ বলুন — (ক) একটি ফাংশন লেখায় কয়টি কমা আছে গুনে ফেরত দেয় (খ) struct Customer-এর name member (গ) একটি ফাংশন দুটি নাম জুড়ে পুরো নাম ফেরত দেয় (ঘ) একটি ফাংশন ফাইলের নাম নিয়ে C-এর fopen ডাকে।

    (ক) প্যারামিটার std::string_view — শুধু পড়ে, এখনই কাজ শেষ। (খ) std::string — struct নামটি রেখে দেবে, তাই মালিক হতে হবে; view রাখলে যে লেখা থেকে নাম এসেছে তা মরে গেলে Customer-এর নাম dangling. (গ) ফেরত std::string — নতুন লেখা তৈরি হচ্ছে, তার মালিক লাগবে; প্যারামিটার দুটি string_view হতে পারে। (ঘ) const std::string& নিয়ে name.c_str() দিন — fopen-এর লাগে NUL-এ শেষ হওয়া লেখা, string_view তার নিশ্চয়তা দেয় না।

  3. বাগ শিকার: একটি AI এই কোড দিয়েছে — std::string_view cityOf(const std::string& line) { auto pos = line.rfind(','); return line.substr(pos + 1); }, আর ব্যবহার int age = std::stoi(line.substr(5, 2));. কয়টি সমস্যা খুঁজে পান?

    তিনটি। (১) line.substr(...) একটি temporary std::string বানায়, আর ফাংশন তার view ফেরত দেয় — ফেরার আগেই temporary মরে, view dangling. ঠিক রূপ: ফেরত টাইপ std::string, বা std::string_view(line).substr(pos + 1) (তখন view আসল line-এর দিকে তাকায়, আর line ডাকার মানুষের কাছে বেঁচে থাকে)। (২) কমা না থাকলে rfind দেয় npos, আর npos + 1 হয় 0 — নিঃশব্দে পুরো লাইন ফেরত। npos যাচাই দরকার। (৩) stoi try/catch ছাড়া, আর অবস্থান 5 হাতে বসানো — নাম লম্বা হলে ভুল ঘর পড়বে বা exception আসবে; অংশ ৫-এর মতো কমা ধরে ভাগ করুন।

  4. নিজে লিখুন (কোড): std::size_t countWords(std::string_view line) লিখুন, যা স্পেস দিয়ে আলাদা শব্দ গোনে; একাধিক স্পেস বা শুরু-শেষের স্পেস থাকলেও ঠিক উত্তর দেবে। " Rafi lives in Dhaka "-এ উত্তর 4 আসা উচিত।

    একটি সহজ উপায়: একটি bool inWord = false রাখুন; প্রতিটি char c-এ, স্পেস হলে inWord = false, নইলে যদি inWord মিথ্যা ছিল তাহলে ++count আর inWord = true. অর্থাৎ শুধু “স্পেস থেকে অক্ষরে ঢোকার” মুহূর্ত গোনা হয়। বিকল্প: std::istringstream in{std::string(line)}; std::string w; while (in >> w) ++count; — >> নিজেই সব whitespace বাদ দেয়, তবে এতে একটি কপি আর stream-এর খরচ আছে। বাংলা লেখাতেও প্রথম উপায় কাজ করে, কারণ স্পেস UTF-8-এ এক byte (0x20), আর কোনো বাংলা byte কখনো 0x20 হয় না।

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

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

ক্রমএকটি std::string-এ += করার সময় জায়গা ফুরিয়ে গেলে যা ঘটে, সেই ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. নতুন size বর্তমান capacity-কে ছাড়িয়ে যায়
  2. heap থেকে নতুন, বড় একটি মেমরি-খণ্ড নেওয়া হয়
  3. পুরোনো অক্ষরগুলো নতুন খণ্ডে কপি হয়, তারপর নতুন অক্ষর যোগ হয়
  4. পুরোনো মেমরি-খণ্ড ফেরত দেওয়া হয়
  5. পুরোনো জায়গার দিকে তাকানো pointer বা string_view অবৈধ হয়ে যায়

UTF-8-এ সেভ করা C++ ফাইলে std::string s = "বাংলা"; লিখলে s.size() কত হয়?

  • 5
  • 2 বা 3
  • 15
  • 10

C++-এ std::string-এর find খোঁজা লেখাটি না পেলে কী ফেরত দেয়?

  • -1
  • std::string::npos
  • 0
  • std::out_of_range exception ছোড়ে

নিচের কোন C++ লাইনটি compile হয় না?

  • auto a = std::string("Dhaka") + "-1205";
  • std::string b = "Dhaka";
  • auto c = "Dhaka-" + std::string("1205");
  • auto d = "Dhaka" + "-1205";

C++-এ std::string_view v = std::string("Sylhet") + "-3100"; লেখার পরের লাইনে v পড়লে কী হয়?

  • Undefined behavior — v একটি ধ্বংস হওয়া temporary-র দিকে তাকিয়ে আছে
  • temporary-র আয়ু v-এর সমান হয়, তাই সব ঠিক থাকে
  • v লেখাটির একটি নিজস্ব কপি রাখে, তাই নিরাপদ
  • সবসময় compile error হয়

C++-এ std::stoi("twenty") ডাকলে কী হয়?

  • 0 ফেরত দেয়
  • একটি error code ফেরত দেয়
  • std::invalid_argument exception ছোড়ে
  • compile error হয়

C++-এ ৫ অক্ষরের std::string s = "Dhaka";-এ s.at(10) আর s[10]-এর পার্থক্য কী?

  • দুটোই একই কাজ করে, শুধু লেখার ধরন আলাদা
  • at(10) std::out_of_range ছোড়ে, s[10] undefined behavior
  • s[10] exception ছোড়ে, at(10) undefined behavior
  • দুটোই string-কে ১১ অক্ষরে বাড়িয়ে নেয়

শব্দকোষ

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

মনে রাখুন

  1. std::string মালিক, std::string_view দর্শক। রেখে দিতে বা বদলাতে string; শুধু এখনই পড়তে string_view.
  2. C string-এর আকার, NUL আর কপির দায় std::string নিজে নেয়; C++ কোডে strcpy/char[] দেখলে প্রশ্ন করুন।
  3. find-এর ফল সবসময় npos-এর সাথে মেলান, তারপর substr; সীমার বাইরে নিরাপদে পড়তে at().
  4. লেখা থেকে সংখ্যা: stoi-তে try/catch, বা from_chars-এ ec যাচাই। reallocation-এ পুরোনো ঠিকানা মরে — view থাকা অবস্থায় মালিক বদলাবেন না।
  5. UTF-8-এ size() = byte. “বাংলা” ১৫ byte, ৫ code point; s[0] একটি byte মাত্র।

পরের পর্বে: এই পর্বে বারবার “ঠিকানা” শব্দটি এসেছে — string_view-এর ভেতরে একটি ঠিকানা, reallocation-এ ঠিকানা বদল। এবার সেই ঠিকানাকেই সরাসরি হাতে নেওয়া: pointer, * আর &, nullptr, আর কেন আধুনিক C++ মালিকানা বোঝাতে raw pointer যতটা সম্ভব এড়িয়ে চলে।

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

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