এই পর্বে যা শিখবেন
- পুরোনো
enum-এর দুটি বিপদ, আরenum classকীভাবে দুটিই দূর করে - underlying type বেছে নেওয়া, আর
switch-এ ভুলে যাওয়া case ধরিয়ে দেওয়া সতর্কবার্তা structবানানো,{}আর designated initializer দিয়ে মান সাজানো, ডিফল্ট মান আর member functionstd::pair,std::tupleআর structured bindings — কখন কোনটি, আর কখন নাম দেওয়া struct ভালো- C++20-এর
= defaultদিয়ে struct তুলনা, আর AI-এর লেখা কোডে “সংখ্যার কোড” ও নামহীন জোড়া ধরা
১ · নিজের টাইপ — কোড যখন সমস্যার ভাষায় কথা বলে
পর্ব ০৫-এ আপনি int, double, bool-এর মতো তৈরি বাক্স চিনেছেন। এগুলো দিয়ে সবই লেখা যায় — কিন্তু “লেখা যায়” আর “পড়ে বোঝা যায়” এক জিনিস নয়। একটি খাবার-ডেলিভারি অ্যাপের কোডের এই লাইনটি দেখুন:
if (order[2] == 3 && order[4] > 0) notify(order[0]);এটি হয়তো ঠিকঠাক চলে। কিন্তু 3 মানে কী? order[2] কোন তথ্য? এক মাস পরে আপনি নিজেও মনে করতে পারবেন না। এবার একই কাজ নিজের টাইপ দিয়ে:
if (order.status == Status::Delivered && order.tip > 0) notify(order.customer);কোনো comment লাগল না। লাইনটি নিজেই বলছে: “অর্ডার পৌঁছে গেলে আর বকশিশ থাকলে গ্রাহককে জানাও।” প্রোগ্রামারের হাতে বানানো এমন টাইপকে বলে user-defined type. এই পর্বে আপনি এর তিনটি সবচেয়ে সরল রূপ শিখবেন:
🏷️ enum class
অল্প কয়েকটি নির্দিষ্ট অবস্থার মধ্যে একটি — অর্ডারের অবস্থা, সপ্তাহের দিন, পেমেন্টের ধরন। “সংখ্যার কোড”-এর বদলে নাম।
একটি থেকে বেছে নেওয়া📦 struct
কয়েকটি সম্পর্কিত তথ্য এক বাক্সে — একজন ছাত্রের নাম, রোল আর নম্বর; একটি অর্ডারের খাবার, দাম আর অবস্থা।
অনেকগুলো একসাথে🔓 structured binding
বাক্স খুলে ভেতরের জিনিসগুলোকে এক লাইনে আলাদা নাম দেওয়া — auto [min, max] = ....
নিজের টাইপ বানানোর আসল লাভ শুধু পড়ার সুবিধা নয়। টাইপ মানে নিয়ম. যখন status একটি int, তখন এতে -7 বা 9999 রাখা compiler-এর চোখে পুরোপুরি বৈধ। যখন এটি একটি Status, তখন এতে শুধু চারটি নির্দিষ্ট মানের একটি রাখা যায় — বাকি সব ভুল compiler নিজেই আটকে দেয়। পর্ব ০২-এ যে “বস্তু দিয়ে ভাবা” শিখেছিলেন, তার প্রথম ব্যবহারিক ধাপ এটিই।
২ · পুরোনো enum — নাম ছড়িয়ে পড়ে, সংখ্যা হয়ে যায়
C থেকে C++ পেয়েছে enum (enumeration — “গুনে গুনে তালিকা”)। এটি কয়েকটি নামকে একটি টাইপে বাঁধে, আর প্রতিটি নামের পেছনে একটি পূর্ণসংখ্যা রাখে — প্রথমটি 0, পরেরটি 1, এভাবে। তালিকার প্রতিটি নামকে বলে enumerator.
enum OrderStatus { Pending, Delivered }; // Pending = 0, Delivered = 1সংখ্যার চেয়ে এটি অনেক ভালো। কিন্তু এই পুরোনো রূপের, যাকে এখন বলে unscoped enum বা plain enum, দুটি বড় দুর্বলতা আছে। এক, এর নামগুলো চারপাশের scope-এ ছড়িয়ে পড়ে — যেন একটি বাড়ির সব জিনিস রাস্তায় রাখা। দুই, এটি চুপচাপ int-এ বদলে যায়, তাই অর্থহীন হিসাবও compile হয়ে যায়। একটি ডেলিভারি অ্যাপে অর্ডারের অবস্থা আর পেমেন্টের অবস্থা — দুটোতেই “Pending” থাকা খুব স্বাভাবিক। কী ঘটে, ধাপে ধাপে দেখুন:
1enum OrderStatus { Pending, Delivered };2enum PaymentStatus { Pending, Paid };3int total = Delivered + 100;4enum class Order { Pending, Delivered };5enum class Payment { Pending, Paid };6Order o = Order::Pending;7int n = Order::Delivered + 100;8int code = static_cast<int>(o);
| খোলা নাম | |
| মান |
- প্রথম plain enum.
PendingআরDeliveredনাম দুটি সরাসরি বাইরের scope-এ চলে এল। এখন ফাইলের যেকোনো জায়গায় শুধুPendingলিখলেই এটি বোঝাবে। - দ্বিতীয় enum — সংঘর্ষ। পেমেন্টের অবস্থাতেও
Pendingদরকার। কিন্তু নামটি আগেই বাইরে দখল হয়ে আছে, তাই compiler আটকে দেয়। পুরোনো দিনে সমাধান ছিল নামের আগে উপসর্গ জোড়া —OS_Pending,PS_Pending— দেখতে খারাপ, ভুলে যাওয়াও সহজ। - নিঃশব্দে সংখ্যা।
Delivered + 100— “পৌঁছে গেছে” যোগ ১০০ মানে কী? কোনো অর্থ নেই, তবু compile হয়, কারণ plain enum নিজে থেকেintহয়ে যায় (Delivered= 1, তাই 101). টাইপের পাহারা এখানে কাজই করল না। enum class— প্রত্যেকের নিজের ঘর। লাইন ৪ আর ৫-এ একই নামPendingদুবার, কিন্তু কোনো সংঘর্ষ নেই। নামগুলো এখনOrderআরPayment-এর ভেতরে বন্দি, বাইরে কিছুই আসেনি।- পুরো ঠিকানা লিখে ডাকা।
Order::Pending— ঠিক যেমন namespace-এর নাম জুড়েstd::coutবা পর্ব ১১-এরshop::money::Takaলেখেন। লেখা একটু লম্বা, কিন্তু পড়লেই বোঝা যায় কোন Pending. - অর্থহীন হিসাব এবার আটকায়।
Order::Delivered + 100compile হয় না।enum classনিজে থেকেintহয় না, তাই “অবস্থা যোগ সংখ্যা” ধরনের ভুল প্রোগ্রাম চালানোর আগেই ধরা পড়ে। - সত্যিই সংখ্যা দরকার হলে — লিখে বলুন। ডেটাবেসে বা নেটওয়ার্কে পাঠাতে সংখ্যা লাগতে পারে। তখন static_cast দিয়ে স্পষ্ট করে বদলান:
Order::Pendingথেকে 0. রূপান্তর হলো, কিন্তু জেনেশুনে — কোড রিভিউতে চোখে পড়ে।
খেয়াল করুন, দ্বিতীয় সমস্যাটি প্রথমটির চেয়েও বিপজ্জনক। নামের সংঘর্ষ অন্তত compile error দেয়। কিন্তু নিঃশব্দে int হয়ে যাওয়া কোনো শব্দ করে না — if (status == 1), status * 2, এমনকি দুটি ভিন্ন enum-এর তুলনা (orderStatus == paymentStatus), সবই compile হয়ে যায়; GCC বড়জোর সতর্কবার্তা দেয়। অনেক বছরের পুরোনো C++ কোডে এ ধরনের বাগ এখনো লুকিয়ে আছে।
৩ · enum class — নিজের ঘরে থাকা, কঠোর টাইপের নাম
C++11 থেকে আসা enum class (একে scoped enum-ও বলে; enum struct লিখলেও হুবহু একই জিনিস) দুটি সমস্যাই একসাথে সমাধান করে। আধুনিক C++-এ নতুন কোডে এটিই ডিফল্ট পছন্দ। খাবার-ডেলিভারি অ্যাপের অর্ডারের চারটি অবস্থা দিয়ে শুরু করা যাক:
enum class Status { Placed, Cooking, OnTheWay, Delivered };
Status s = Status::Cooking; // ঠিক আছে
s = Status::OnTheWay; // ঠিক আছে — অবস্থা বদলাল
s = 2; // error: int থেকে Status-এ নিজে থেকে যাওয়া যায় না
if (s == Status::Delivered) { } // একই enum-এর মধ্যে তুলনা চলেঢাকার একটি বড় অফিস-ভবনের কথা ভাবুন। প্রতিটি অফিসে “রিসেপশন” আছে। কেউ যদি শুধু “রিসেপশনে যান” বলেন, কোনটা বোঝাবে? তাই বলা হয় “পাঁচতলার রিসেপশন”, “আটতলার রিসেপশন”। enum class ঠিক এটাই করে — Order::Pending আর Payment::Pending দুই তলার দুই রিসেপশন। আর প্রতিটি অফিসের গেটে পাহারাদার আছে: বাইরের সংখ্যা ঢুকতে চাইলে পরিচয়পত্র (static_cast) দেখাতে হয়।
Underlying type — enum মেমরিতে কত বড়
প্রতিটি enum-এর পেছনে আসলে একটি পূর্ণসংখ্যা থাকে, যাকে বলে underlying type. enum class-এ ডিফল্টভাবে এটি int (সাধারণত ৪ বাইট)। কিন্তু চারটি অবস্থা রাখতে ৪ বাইট লাগে না। লক্ষ লক্ষ অর্ডার মেমরিতে বা ফাইলে রাখলে, বা নেটওয়ার্কে পাঠালে, আপনি নিজে আকার বেছে নিতে পারেন:
enum class Status : std::uint8_t { Placed, Cooking, OnTheWay, Delivered }; // ১ বাইট
enum class HttpCode : int { Ok = 200, NotFound = 404, ServerError = 500 }; // নিজে মান দেওয়াদ্বিতীয় লাইনটি দেখায়, enumerator-কে নিজে মানও দেওয়া যায় — বাইরের কোনো নিয়ম (যেমন HTTP-র 404) মেলাতে এটা কাজে লাগে। আরেকটি সুবিধা: underlying type জানা থাকলে enum-কে আগে থেকে ঘোষণা করে রাখা যায় (enum class Status : std::uint8_t;) — পুরো তালিকা না দিয়েই। পর্ব ১১-এর header ফাইলে এটা কাজে লাগে, কারণ তালিকা বদলালে সব ফাইল নতুন করে compile করতে হয় না।
Enum থেকে সংখ্যা — std::to_underlying
static_cast<int>(s) কাজ করে, কিন্তু একটি ঝুঁকি আছে: কেউ পরে underlying type বদলে std::int64_t করলে আপনার int cast চুপচাপ ভুল আকারে বদলাবে। C++23 থেকে <utility>-এর std::to_underlying(s) ঠিক underlying type-টিই ফেরত দেয় — আকার নিয়ে আর ভাবতে হয় না। নিচের প্রোগ্রামে অর্ডারের অবস্থা, তার লেবেল, কোড আর আকার একসাথে দেখুন:
#include <cstdint>
#include <print>
#include <string_view>
#include <utility>
enum class Status : std::uint8_t { Placed, Cooking, OnTheWay, Delivered };
std::string_view label(Status s) {
switch (s) {
case Status::Placed: return "Order placed";
case Status::Cooking: return "Restaurant is cooking";
case Status::OnTheWay: return "Rider is on the way";
case Status::Delivered: return "Delivered, enjoy!";
}
return "Unknown"; // enum-e baje man thakle
}
int main() {
for (Status s : {Status::Placed, Status::OnTheWay, Status::Delivered})
std::println("[{}] {}", std::to_underlying(s), label(s));
std::println("sizeof(Status) = {} byte", sizeof(Status));
}আউটপুটে আসবে [0] Order placed, [2] Rider is on the way, [3] Delivered, enjoy! আর শেষে sizeof(Status) = 1 byte — কারণ underlying type std::uint8_t. label ফাংশনে চারটি অবস্থার প্রতিটির জন্য একটি case আছে, তাই compiler খুশি। switch-এর পরের return "Unknown"; লাইনটি অপ্রয়োজনীয় মনে হতে পারে, কিন্তু এটি না থাকলে GCC সতর্ক করে control reaches end of non-void function — কারণ static_cast<Status>(7)-এর মতো তালিকার বাইরের মানও প্রযুক্তিগতভাবে একটি Status-এ রাখা যায়। {Status::Placed, ...} লেখাটি range-for দিয়ে কয়েকটি মানের ওপর হাঁটার সংক্ষিপ্ত উপায় (পর্ব ০৮)।
std::uint8_t আসলে unsigned char-এর আরেক নাম। তাই std::cout << std::to_underlying(s) লিখলে সংখ্যা নয়, একটি অক্ষর (বা অদৃশ্য নিয়ন্ত্রণ-চিহ্ন) ছাপা হয়! std::println/std::format unsigned char-কে সংখ্যা হিসেবেই দেখায়, তাই উপরের কোডে সমস্যা নেই। cout ব্যবহার করলে সামনে একটি + দিন (+std::to_underlying(s)) বা int-এ cast করুন।
switch আর ভুলে যাওয়া case — compiler-এর পাহারা
enum class আর switch একসাথে খুব শক্তিশালী, কারণ compiler জানে তালিকায় ঠিক কোন কোন মান আছে। ফলে কোনো case বাদ পড়লে সে মনে করিয়ে দিতে পারে। বাস্তব সফটওয়্যারে এটা কতটা জরুরি, নিচের গল্পে দেখুন:
enum class Status { Placed, Cooking, OnTheWay, Delivered };
switch (s) {
case Status::Placed: return "Order placed";
case Status::Cooking: return "Cooking";
case Status::OnTheWay: return "On the way";
case Status::Delivered: return "Delivered";
}
g++ -Wall: কোনো সতর্কবার্তা নেই
enum class Status { Placed, Cooking, OnTheWay, Delivered, Cancelled };
switch (s) { // আগের মতোই চারটি case
...
}
warning: enumeration value 'Cancelled' not handled in switch [-Wswitch]
switch (s) {
case Status::Placed: return "Order placed";
...
default: return "Unknown";
}
কোনো সতর্কবার্তা নেই — কিন্তু বাতিল অর্ডার দেখায় "Unknown"
switch (s) {
case Status::Placed: return "Order placed";
...
case Status::Delivered: return "Delivered";
case Status::Cancelled: return "Cancelled";
}
সতর্কবার্তা নেই, আর আচরণও সঠিক
switch (s) {
using enum Status;
case Placed: return "Order placed";
case Cooking: return "Cooking";
...
}
শুধু এই switch-এর ভেতরে Status:: লিখতে হয় না
- শুরুতে সব ঠিক। চারটি অবস্থা, চারটি
case. প্রতিটি অবস্থার জন্য লেবেল আছে, compiler কিছু বলে না। - তিন মাস পরে নতুন নিয়ম। ব্যবসার দল জানাল, গ্রাহক অর্ডার বাতিল করতে পারবেন — enum-এ যোগ হলো
Cancelled. কিন্তু যিনি enum বদলালেন, তিনি জানতেন না যে অন্য ফাইলে এইswitchআছে।-Wall-এর অংশ -Wswitch সাথে সাথে জানিয়ে দিল কোন মানটি বাদ পড়েছে — যত ফাইলে যতswitchআছে, সবখানে। - লোভনীয় কিন্তু ভুল পথ। কেউ সতর্কবার্তা থামাতে
default:যোগ করলেন। সতর্কবার্তা গেল, কিন্তু বাগ রয়ে গেল — বাতিল অর্ডার এখন দেখায় “Unknown”. আরও খারাপ, ভবিষ্যতে আবার নতুন অবস্থা যোগ হলে compiler আর কিছুই বলবে না। enum-এর ওপর switch-এ সাধারণতdefaultদেবেন না; তালিকার বাইরের মান সামলাতে switch-এর পরে একটি return রাখুন, যেমন উপরের কোডে। - সঠিক পথ। নতুন অবস্থার জন্য নতুন
case. এখানেই সতর্কবার্তার আসল মূল্য: compiler আপনার হয়ে পুরো প্রজেক্টে খুঁজে দিল কোথায় কোথায় সিদ্ধান্ত নিতে হবে। (যেসব দলdefaultরাখতেই চান, তাঁদের জন্য GCC-র-Wswitch-enumআছে, যাdefaultথাকলেও বাদ পড়া মান জানায়।) - লেখা ছোট করা — C++20. প্রতিটি case-এ
Status::লেখা ক্লান্তিকর। C++20 থেকেusing enum Status;লিখলে শুধু সেই জায়গায় (এখানে switch-এর ভেতরে) নামগুলো সরাসরি ব্যবহার করা যায়। scope সীমিত থাকে, তাই plain enum-এর মতো নাম ছড়িয়ে পড়ার সমস্যা ফিরে আসে না।
C-এর enum আর C++-এর plain enum প্রায় একই — দুটোরই নাম বাইরে ছড়ায়, দুটোই সংখ্যা হয়ে যায়; C-তে তো উল্টো দিকেও (int থেকে enum-এ) নিঃশব্দে যাওয়া যায়, C++-এ যায় না। C-তে enum class নেই। struct-এ বড় পার্থক্য হলো, C++-এ struct Student বানালে শুধু Student লিখেই টাইপটি ব্যবহার করা যায় — C-এর মতো typedef লাগে না — আর struct-এর ভেতরে ফাংশনও রাখা যায়। Member-এর ক্রম, padding আর মেমরির ছবি গভীরভাবে জানতে C কোর্সের struct ও union পর্বটি দেখুন।
৪ · struct — কয়েকটি তথ্যকে এক বাক্সে বাঁধা
একজন ছাত্রের তথ্য রাখতে চান: নাম, রোল, নম্বর। struct ছাড়া লিখতে হতো তিনটি আলাদা ভ্যারিয়েবল — name1, roll1, marks1, তারপর name2, roll2… ফাংশনে পাঠাতে তিনটি parameter, ফেরত দিতে আরও ঝামেলা। আর কোন marks কোন name-এর, সেটা শুধু ভ্যারিয়েবলের নামের ভরসায়।
struct এই তিনটিকে এক বাক্সে বাঁধে। বাক্সের ভেতরের প্রতিটি ভ্যারিয়েবলকে বলে data member, আর পড়া হয় ডট (.) দিয়ে:
struct Student {
std::string name;
int roll{};
int marks{};
}; // শেষের সেমিকোলন ভুলবেন না!
Student rafi{"Rafi", 7, 85};
std::println("{} got {}", rafi.name, rafi.marks);বোর্ড পরীক্ষার প্রবেশপত্রের কথা ভাবুন। একটি কার্ডেই নাম, রোল, রেজিস্ট্রেশন নম্বর আর কেন্দ্রের নাম ছাপা থাকে। কেউ যদি নাম একটি কাগজে, রোল আরেক কাগজে দিত — হারিয়ে যেত, মিলিয়ে ফেলা কঠিন হতো। struct হলো সেই এক-কার্ড: যা একসাথে থাকা উচিত, তা একসাথে থাকে, একসাথে ফাংশনে যায়, একসাথে ফেরত আসে।
তিন উপায়ে মান সাজানো
উপরের Student একটি aggregate — নিজের কোনো constructor নেই, সব member বাইরে থেকে দেখা যায়। এমন struct-কে {}-এর ভেতরে মান সাজিয়ে বানানো যায়, একে বলে aggregate initialization. C++20 থেকে আরেকটি উপায় এসেছে — designated initializer, যেখানে প্রতিটি মানের আগে member-এর নাম লেখা হয়। আর struct-এর ভেতরেই ডিফল্ট মান লিখে রাখা যায় — default member initializer. একটি খাবার-অর্ডারের struct দিয়ে সবগুলো দেখুন:
1struct Order {2 std::string item;3 int qty{1};4 double price{};5 bool paid{false};6};7Order a{};8Order b{"Biryani", 2, 250.0, true};9Order c{.item = "Fuchka", .price = 60.0};10Order d{.qty = 3, .item = "Tea"};11Order e{"Kacchi", .qty = 2};12Order f;
| .item | |
| .qty | |
| .price | |
| .paid |
- খালি
{}. কোনো মান দেওয়া হয়নি, তাই প্রতিটি member পেল struct-এর ভেতরে লেখা ডিফল্ট মান:qty1,price0.0,paidfalse.std::stringনিজেই খালি লেখা দিয়ে শুরু হয়। - ক্রম ধরে সব মান।
{"Biryani", 2, 250.0, true}— মানগুলো member-দের ঘোষণার ক্রমে বসে: প্রথমটিitem, দ্বিতীয়টিqty… কাজ করে, কিন্তু পড়ে বোঝা কঠিন:2কি পরিমাণ, না টেবিল নম্বর? আর কেউ struct-এ member-এর ক্রম বদলালে এই লাইন নিঃশব্দে ভুল মানে বদলে যেতে পারে। - নাম ধরে — designated initializer.
{.item = "Fuchka", .price = 60.0}— যা দিলেন তা নাম দেখেই পরিষ্কার।qtyআরpaidদেননি, তাই তারা নিজেদের ডিফল্ট মান পেল (1 আর false). কয়েকটি member দিয়ে বাকি ডিফল্ট রাখার জন্য এটাই সবচেয়ে পরিষ্কার লেখা। - ক্রম উল্টালে — error. C++20-এর নিয়ম: designator-গুলো struct-এ ঘোষণার ক্রমেই লিখতে হবে। এখানে
.qtyআগে,.itemপরে — অথচ struct-এitemআগে। কারণ C++-এ member সবসময় ঘোষণার ক্রমেই তৈরি হয়; লেখার ক্রম আলাদা হলে পাঠক ভুল বুঝতেন। (C ভাষায় যেকোনো ক্রমে লেখা যায় — এখানে C আর C++ আলাদা।) - মেশালে — error. কিছু মান ক্রম ধরে, কিছু নাম ধরে — C++20 এটা মানে না। হয় সব নাম ধরে, নয় সব ক্রম ধরে।
- কোনো
{}-ই নেই।Order f;— পর্ব ০৫-এ দেখেছিলেন,int x;লিখলে আবর্জনা মান আসে। কিন্তু এখানে প্রতিটি সংখ্যার member-এর ডিফল্ট মান struct-এই লেখা আছে, তাইf-ও নিরাপদ। এটাই default member initializer-এর সবচেয়ে বড় উপহার: struct-এর ব্যবহারকারী ভুলে গেলেও কোনো ঘর খালি থাকে না।
ডিফল্ট মান না দিলে কী হয়? struct Raw { int qty; double price; }; হলে Raw r;-এ দুটি ঘরেই আবর্জনা মান, আর তা পড়া undefined behavior. Raw r{}; লিখলে অবশ্য দুটোই শূন্য হয়, আর Raw r{5};-এ বাদ পড়া price-ও শূন্য পায় — তবে -Wextra চালু থাকলে GCC সতর্ক করে missing initializer for member 'Raw::price'. ডিফল্ট মান লিখে রাখলে এই সতর্কবার্তাও আসে না, আর কেউ {} ভুলে গেলেও বিপদ নেই। তাই নিয়ম: struct-এর প্রতিটি সংখ্যা, bool বা pointer member-কে ডিফল্ট মান দিন।
ছোট একটি ইতিহাস — aggregate-এর নিয়ম বদলেছে
C++11-এ struct-এর ভেতরে ডিফল্ট মান লেখার সুযোগ এলেও তখন একটি অদ্ভুত নিয়ম ছিল: ডিফল্ট মান দিলেই struct আর aggregate থাকত না, ফলে {"Rafi", 7, 85} লেখা যেত না! C++14 এই নিয়ম তুলে দেয়। তাই পুরোনো বই বা ফোরামে “ডিফল্ট মান দিলে brace দিয়ে বানানো যায় না” পড়লে বুঝবেন, সেটা C++11-এর কথা।
৫ · Member function — struct-এর নিজের কাজ, আর class-এর সাথে সম্পর্ক
ধরুন বারবার জানতে হয় একজন ছাত্র পাস করেছেন কি না (৪০ বা তার বেশি)। একটি আলাদা ফাংশন bool passed(const Student& s) লেখা যায়। কিন্তু এই প্রশ্নটি তো ছাত্রেরই — তাই C++ ফাংশনটিকে struct-এর ভেতরেই রাখতে দেয়। এমন ফাংশনকে বলে member function:
struct Student {
std::string name;
int marks{};
bool passed() const { return marks >= 40; } // নিজের marks সরাসরি দেখে
char grade() const {
if (marks >= 80) return 'A';
if (marks >= 60) return 'B';
return marks >= 40 ? 'C' : 'F';
}
};
Student s{.name = "Mitu", .marks = 72};
if (s.passed()) std::println("{} passed with {}", s.name, s.grade());লক্ষ করুন, ফাংশনের ভেতরে marks লিখতে s.marks লাগেনি — member function যে অবজেক্টের ওপর ডাকা হয় (s.passed()), তার member-গুলো সরাসরি দেখতে পায়। নামের পরের const একটি প্রতিশ্রুতি: “এই ফাংশন অবজেক্টের কিছু বদলাবে না।” এর বিস্তারিত পর্ব ২০-এ। একটি বিষয় জেনে রাখুন: member function যোগ করলেও Student aggregate-ই থাকে — designated initializer, structured binding সবই চলে, কারণ এগুলো শুধু data member দেখে।
struct আর class — পার্থক্য মাত্র একটি শব্দের
অনেক বইয়ে পড়বেন “struct-এ শুধু ডেটা থাকে, class-এ ফাংশনও থাকে।” C-তে এটা সত্যি, C++-এ নয়। C++-এ struct আর class প্রায় হুবহু এক যন্ত্র — দুটোতেই ডেটা, ফাংশন, constructor, inheritance সব চলে। পার্থক্য শুধু ডিফল্ট access specifier-এ:
| বিষয় | struct | class |
|---|---|---|
| member-দের ডিফল্ট access | public — বাইরে থেকে সবাই দেখে | private — শুধু নিজের ফাংশন দেখে |
| inheritance-এর ডিফল্ট (পর্ব ২৪) | public | private |
| প্রচলিত ব্যবহার | সরল ডেটার বাক্স, যার member-গুলো স্বাধীনভাবে যেকোনো মান নিতে পারে | এমন টাইপ, যার একটি নিয়ম (invariant) রক্ষা করতে হয় — যেমন ব্যাংক হিসাবের ব্যালান্স কখনো ঋণাত্মক নয় |
শেষ সারিটি নিয়ম নয়, রীতি — কিন্তু খুব শক্ত রীতি। Student-এর নাম যেকোনো লেখা, নম্বর যেকোনো সংখ্যা হতে পারে; বাইরের কেউ বদলালেও কিছু ভাঙে না — তাই struct. কিন্তু একটি bKash ওয়ালেটের ব্যালান্স যদি যে কেউ সরাসরি wallet.balance = -500 লিখতে পারে, সর্বনাশ। সেখানে ডেটা private রেখে শুধু deposit(), withdraw() ফাংশন দিয়ে বদলাতে দিতে হয় — সেটাই class, পর্ব ২০-এর বিষয়।
মেমরিতে struct কত বড়? সব member-এর আকারের যোগফলের চেয়ে বেশি হতে পারে। struct A { char a; int b; char c; };-এ member-দের আকারের যোগফল ৬ বাইট, কিন্তু 64-bit Linux-এ sizeof(A) সাধারণত ১২. কারণ int-কে ৪-এর গুণিতক ঠিকানায় বসাতে compiler মাঝে ফাঁকা বাইট দেয় — একে বলে padding. Member-গুলো বড় থেকে ছোট ক্রমে সাজালে (int b; char a; char c;) একই তথ্য ৮ বাইটে ধরে। লাখো অবজেক্ট হলে এই পার্থক্য চোখে পড়ার মতো। ছবিসহ পূর্ণ ব্যাখ্যা আছে C কোর্সের struct ও union পর্বের alignment অংশে — নিয়মগুলো C++-এও একই।
৬ · std::pair আর std::tuple — নামহীন জোড়া
কখনো কখনো দুটি মান একসাথে ফেরত দিতে হয়, আর শুধু সেজন্য একটি struct বানাতে আলসেমি লাগে। Standard library-তে এর জন্য তৈরি টাইপ আছে। <utility>-এর std::pair যেকোনো দুটি মান রাখে, যাদের নাম সবসময় first আর second. <tuple>-এর std::tuple যেকোনো সংখ্যক মান রাখে, যাদের কোনো নামই নেই — পড়তে হয় ক্রমিক নম্বর দিয়ে:
std::pair<std::string, int> topper{"Mitu", 97};
std::println("{} {}", topper.first, topper.second);
std::tuple<std::string, int, double> rec{"Rafi", 7, 3.75};
std::println("{} {} {}", std::get<0>(rec), std::get<1>(rec), std::get<2>(rec));
std::pair p{10, 2.5}; // C++17: টাইপ লিখতে হয় না, compiler বুঝে নেয় pair<int, double>এগুলো সত্যিই কাজের — বিশেষ করে generic কোডে (পর্ব ২৮-২৯-এর template), যেখানে আগে থেকে জানা নেই কী কী মান থাকবে। Standard library নিজেও এদের ব্যবহার করে: পর্ব ৩১-এ দেখবেন, std::map-এর প্রতিটি উপাদান আসলে একটি pair (key আর value). কিন্তু নিজের কোডে একটি প্রশ্ন জিজ্ঞেস করুন: পাঠক কি .first আর .second দেখে বুঝবেন কোনটা কী?
⚠️ নামহীন জোড়ার জঞ্জাল
std::pair<std::pair<int,int>,int> best;if (best.first.second > x.second) ...
এটা কি (রোল, নম্বর) আর ক্লাস? নাকি (সারি, কলাম) আর দূরত্ব? লেখক ছাড়া কেউ জানেন না — আর তিন মাস পরে লেখকও না।
✅ নাম দেওয়া struct
struct Cell { int row{}; int col{}; };struct Path { Cell end; int distance{}; };if (best.end.col > x.col) ...
দুই লাইন বেশি লিখলেন, কিন্তু কোড এখন নিজেই নিজের ব্যাখ্যা।
দুটি মান ফেরত দেওয়া — সবচেয়ে কম আর সবচেয়ে বেশি নম্বর
একটি ক্লাসের নম্বরের তালিকা থেকে সর্বনিম্ন আর সর্বোচ্চ — দুটোই একটি ফাংশন থেকে চাই। তিনটি উপায় তুলনা করুন:
| উপায় | ফাংশনের ঘোষণা | মূল্যায়ন |
|---|---|---|
| reference parameter (পর্ব ১০) | void findMinMax(const std::vector<int>& v, int& lo, int& hi) | কাজ করে, কিন্তু ডাকার জায়গায় আগে দুটি খালি ভ্যারিয়েবল বানাতে হয়; ফল কোথায় যায়, ঘোষণা না দেখে বোঝা যায় না |
std::pair | std::pair<int, int> findMinMax(...) | সংক্ষিপ্ত; কিন্তু first কি min, না max? প্রতিবার মনে রাখতে হয় |
| নাম দেওয়া struct | struct MinMax { int min; int max; };MinMax findMinMax(...) | সবচেয়ে পরিষ্কার: r.min, r.max. এক লাইনের struct, তবু ভুলের সুযোগ নেই |
মজার কথা, C++ standard কমিটিও একই সিদ্ধান্তে পৌঁছেছে। পুরোনো std::minmax_element ফল দেয় একটি pair-এ; কিন্তু C++20-এর নতুন std::ranges::minmax ফল দেয় একটি struct-এ, যার member-দের নাম min আর max. নতুন নকশায় কমিটি জেনেশুনেই নামহীন জোড়া থেকে সরে এসেছে।
৭ · Structured bindings — বাক্স খুলে প্রতিটি অংশকে নাম দেওয়া
struct বা pair ফেরত দিলে ডাকার জায়গায় লিখতে হয় auto r = findMinMax(v);, তারপর r.min, r.max. C++17 থেকে এক লাইনেই বাক্স খুলে ফেলা যায়:
auto [lo, hi] = findMinMax(marks); // lo = r.min, hi = r.maxএর নাম structured binding. বন্ধনীর ভেতরের নামগুলো ক্রম অনুযায়ী struct-এর data member-দের সাথে (বা pair-এর first/second, tuple-এর উপাদান, এমনকি array-এর ঘরগুলোর সাথে) জোড়া লাগে। নাম আপনার ইচ্ছামতো — struct-এর member-এর নামের সাথে মিলতে হবে না, শুধু সংখ্যা মিলতে হবে। একটা জিনিস প্রায় সবাই প্রথমবার ভুল বোঝেন: এই নামগুলো কি আসল অবজেক্টকেই বোঝায়, নাকি কপি? ধাপে ধাপে দেখুন:
1Student rafi{.name = "Rafi", .marks = 85};2auto [name, marks] = rafi;3marks = 90;4auto& [n2, m2] = rafi;5m2 = 90;6std::pair<std::string, int> top{"Mitu", 97};7const auto& [who, score] = top;8auto [x, y, z] = rafi;
| rafi.marks | |
| marks (কপি) | |
| m2 (আসলটাই) | |
| who, score |
- একটি ছাত্র। designated initializer দিয়ে
rafiবানানো হলো — নাম Rafi, নম্বর 85. এখানেStudent-এর দুটি data member:nameআরmarks. auto [...]— কপি করে খোলা। compiler আড়ালেrafi-র একটি পুরো কপি বানায়, তারপরnameআরmarksনাম দুটি সেই কপির দুই ঘরকে দেয়। নামগুলো নতুন, ঘরগুলোও নতুন।- কপি বদলালে আসলটা বদলায় না।
marks = 90শুধু কপির ঘর বদলাল।rafi.marksএখনো 85. আপনি যদি ভেবে থাকেন “Rafi-র নম্বর বাড়ালাম” — হয়নি। auto&— আসলটাকেই খোলা। সামনে&দিলে কোনো কপি হয় না;n2আরm2সরাসরিrafi-র নিজের ঘরের আরেক নাম — ঠিক পর্ব ১০-এর reference-এর মতো।- এবার বদল আসল জায়গায়।
m2 = 90দিলেrafi.marks-ই 90 হয়ে গেল। নিয়ম মনে রাখুন: শুধু পড়বেন —const auto&; বদলাবেন —auto&; আলাদা কপি চান —auto. - pair-ও খোলা যায়।
whoপেলtop.first,scoreপেলtop.second. এখন আর কোডে কোথাও.first/.secondলিখতে হবে না — নামহীন জোড়া ডাকার জায়গায় নাম পেয়ে গেল।const auto&হওয়ায় কপিও হয়নি, ভুল করে বদলানোর সুযোগও নেই। - নামের সংখ্যা মিলতেই হবে।
Student-এ দুটি data member, অথচ তিনটি নাম দেওয়া হয়েছে — compile error. কম দিলেও error. কোনো একটি অংশ লাগবে না? এখন পর্যন্ত সব কটিকেই নাম দিতে হয়; C++26 থেকে না-লাগা অংশের জায়গায়_লেখা যাবে।
এবার পুরো ছবিটি একসাথে চালিয়ে দেখুন — একটি Student, designated initializer, structured binding, struct-এর তুলনা আর দুটি মান ফেরত দেওয়া ফাংশন:
#include <print>
#include <string>
#include <vector>
struct Student {
std::string name;
int marks{};
bool operator==(const Student&) const = default; // C++20
};
struct MinMax { int min; int max; };
MinMax findMinMax(const std::vector<int>& v) {
MinMax r{v[0], v[0]};
for (int x : v) { if (x < r.min) r.min = x; if (x > r.max) r.max = x; }
return r;
}
int main() {
Student rafi{.name = "Rafi", .marks = 85};
auto [name, marks] = rafi;
std::println("{} got {}, same as copy? {}", name, marks, rafi == Student{"Rafi", 85});
auto [lo, hi] = findMinMax({72, 85, 39, 91, 64});
std::println("lowest = {}, highest = {}", lo, hi);
}প্রথম লাইনে আসবে Rafi got 85, same as copy? true — rafi-কে খুলে দুটি নাম পাওয়া গেল, আর == দুটি Student-এর প্রতিটি member মিলিয়ে দেখল। দ্বিতীয় লাইনে lowest = 39, highest = 91: findMinMax একটি MinMax ফেরত দিল, আর ডাকার জায়গায় structured binding সেটি খুলে lo, hi নাম দিল। MinMax r{v[0], v[0]} দুটি member-ই ক্রম ধরে পেল, তাই কোনো সতর্কবার্তা নেই। একটি সাবধানতা: খালি vector পাঠালে v[0] undefined behavior — বাস্তব কোডে আগে খালি কি না দেখতে হয় (পর্ব ১৩ আর পর্ব ৩৮-এর std::optional).
পরে যেখানে বারবার দেখবেন — map-এর ওপর লুপ
Structured binding সবচেয়ে বেশি চোখে পড়বে container-এর ওপর লুপে। পর্ব ৩১-এ std::map শিখলে এমন লেখা প্রতিদিন দেখবেন — map-এর প্রতিটি উপাদান একটি pair, আর লুপের মাথাতেই সেটি খুলে নাম দেওয়া হচ্ছে:
std::map<std::string, int> results{{"Mitu", 97}, {"Rafi", 85}};
for (const auto& [name, marks] : results)
std::println("{}: {}", name, marks);
// C++17-এর আগে: for (const auto& p : results) std::println("{}: {}", p.first, p.second);৮ · struct তুলনা — C++20-এর = default
দুটি int-কে == দিয়ে তুলনা করা যায়। দুটি Student-কে? C++20-এর আগে, নিজে না লিখলে, না। a == b লিখলেই error: no match for 'operator=='. কারণ compiler নিজে থেকে ধরে নিতে চায় না “সমান” মানে কী — সব member মেলা? শুধু রোল মেলা? তাই প্রতিটি struct-এর জন্য প্রোগ্রামাররা হাতে লিখতেন:
// C++20-এর আগে — প্রতিটি member হাতে মেলানো (একটি ভুলে গেলেই বাগ)
bool operator==(const Student& a, const Student& b) {
return a.name == b.name && a.marks == b.marks;
}struct-এ নতুন member যোগ হলে এই ফাংশন বদলাতে ভুলে যাওয়া খুব সাধারণ বাগ। C++20 থেকে compiler-কে বলে দেওয়া যায়: “সব member ক্রম ধরে মিলিয়ে তুলনাটা নিজেই বানিয়ে নাও।” একে বলে defaulted comparison:
#include <compare>
struct Student {
std::string name;
int marks{};
auto operator<=>(const Student&) const = default; // ==, !=, <, >, <=, >= সব পাওয়া গেল
};
Student a{"Rafi", 85}, b{"Rafi", 90};
// a == b -> false, a < b -> true (নাম সমান, তাই marks দেখা হলো: 85 < 90)এখানে নতুন চিহ্ন <=> — দেখতে মহাকাশযানের মতো বলে ডাকনাম “spaceship operator”, আসল নাম three-way comparison. একবারেই জানায় একটি মান অন্যটির চেয়ে ছোট, সমান না বড়; সেখান থেকে compiler বাকি চারটি তুলনা নিজেই বানিয়ে নেয়। = default দিলে তুলনা হয় member-দের ঘোষণার ক্রমে — অভিধানের মতো: আগে name, সমান হলে marks. শুধু == দরকার হলে প্রথম কোডের মতো bool operator==(...) const = default; যথেষ্ট। নিজের মতো করে operator লেখা পর্ব ২৩-এ, আর <=>-এর পূর্ণ গল্প পর্ব ৪২-এ।
enumআরenum class-এর পার্থক্য? —enum class-এর নাম নিজের scope-এ থাকে (Status::Placed) আর নিজে থেকেint-এ বদলায় না; plain enum-এর নাম বাইরে ছড়ায় ও নিঃশব্দেintহয়।- C++-এ
structআরclass-এর পার্থক্য? — শুধু ডিফল্ট access: struct-এpublic, class-এprivate(inheritance-এর ডিফল্টও তাই)। বাকি সব ক্ষমতা এক। enum class-এর ডিফল্ট underlying type? —int;enum class E : std::uint8_tলিখে বদলানো যায়।- Structured binding কী, কবে এসেছে? — C++17;
auto [a, b] = x;দিয়ে struct, pair, tuple বা array-এর অংশগুলোকে আলাদা নাম দেওয়া।autoহলে কপি,auto&হলে আসলটাই। struct { char a; int b; char c; }-এর sizeof ৬ নয় কেন? — alignment-এর জন্য padding; সাধারণ 64-bit সিস্টেমে ১২ বাইট, member বড় থেকে ছোট সাজালে ৮.
৯ · AI-এর যুগে — সংখ্যার কোড আর নামহীন জোড়া
AI টুলগুলো বিশাল পরিমাণ পুরোনো কোড পড়ে শিখেছে, তাই তাদের লেখায় প্রায়ই C আর পুরোনো C++-এর অভ্যাস ফিরে আসে। বিশেষ করে কনটেস্ট-ধাঁচের সমাধানে নিজের টাইপ প্রায় থাকেই না — সবকিছু int আর pair. কোড চলে, পরীক্ষাও পাস করে; সমস্যা ধরা পড়ে যখন অন্য কেউ (বা আপনি নিজে) সেটা পড়তে বা বদলাতে যান। সাধারণ ভুলগুলো:
| AI যা করে | সমস্যা | যা হওয়া উচিত |
|---|---|---|
int status; // 0=placed, 1=cooking... | magic number; status = 7-ও বৈধ, comment পুরোনো হয়ে যায় | enum class Status |
plain enum বা #define PLACED 0 | নাম ছড়ায়, নিঃশব্দে int হয়, ভিন্ন enum মেশানো যায় | enum class, দরকারে std::to_underlying |
pair<pair<int,int>,int>, tuple<int,int,int,int> | .first.second পড়ে কেউ বোঝে না; ক্রম ভুল হলেও compile হয় | নাম দেওয়া ছোট struct |
struct S { int qty; double price; }; — ডিফল্ট মান নেই | S s; লিখলে আবর্জনা মান — undefined behavior | int qty{}; double price{}; |
enum-এর ওপর switch-এ সবসময় default: | নতুন অবস্থা যোগ হলে compiler আর সতর্ক করে না | প্রতিটি মানের case, default ছাড়া |
for (auto [k, v] : bigMap) | প্রতিবার পুরো উপাদানের কপি — ধীর; আর বদলালে আসলটা বদলায় না | const auto& [k, v] |
তুলনার operator== হাতে লেখা | নতুন member যোগ হলে মেলাতে ভুলে যায় | C++20: = default |
AI-কে কাজ দেওয়ার সময় টাইপের নকশা নিয়ে নিয়মগুলো আগে থেকে বলে দিন। এতে কোড পড়তে সহজ হয়, আর ভুল হলে compiler-ই ধরিয়ে দেয়:
AI-এর লেখা struct/enum কোড যাচাইয়ের চেকলিস্ট
১০ · মডিউল ২ শেষ — ভাষার মূল যন্ত্রপাতি এখন আপনার হাতে
এই পর্বের সাথে মডিউল ২ “ভাষার মূল যন্ত্রপাতি” শেষ হলো। সাতটি পর্বে আপনি ভাষার সেই অংশগুলো হাতে নিয়েছেন, যা প্রায় প্রতিটি C++ প্রোগ্রামের প্রতিটি পাতায় থাকে। একবার ফিরে দেখুন:
| পর্ব | বিষয় | এখন আপনি যা পারেন |
|---|---|---|
| ০৬ | অপারেটর | হিসাব, তুলনা আর যুক্তির অপারেটর দিয়ে শর্ত ও সূত্র লিখতে পারেন, কোনটা আগে চলে জানেন |
| ০৭ | ইনপুট-আউটপুট | ব্যবহারকারীর কাছ থেকে মান নিতে আর সুন্দর করে ফল দেখাতে পারেন |
| ০৮ | সিদ্ধান্ত ও লুপ | if, switch আর লুপ দিয়ে প্রোগ্রামের পথ ঠিক করতে পারেন |
| ০৯ | ফাংশন ও overloading | কাজকে ছোট, নাম দেওয়া, বারবার ব্যবহারযোগ্য টুকরোয় ভাগ করতে পারেন |
| ১০ | reference | কপি না করে ফাংশনে বড় ডেটা পাঠাতে, আর দরকার হলে আসল মান বদলাতে পারেন |
| ১১ | namespace আর header | প্রোগ্রামকে কয়েকটি ফাইলে সাজাতে আর নামের সংঘর্ষ এড়াতে পারেন |
| ১২ | enum class, struct আর structured bindings | নিজের টাইপ বানিয়ে কোডকে সমস্যার ভাষায় লিখতে পারেন — সংখ্যার কোড আর নামহীন জোড়া ছাড়াই |
এই কয়টি দিয়েই অনেক বাস্তব সমস্যা লেখা যায়।
এতদিন আপনার প্রোগ্রাম একটি-দুটি মান নিয়ে কাজ করেছে। কিন্তু বাস্তবের ডেটা আসে তালিকায় — ক্লাসের ষাট জন ছাত্র, দিনের হাজার অর্ডার। মডিউল ৩ শুরু হবে ঠিক সেখান থেকে: এক সারিতে অনেক মান রাখা, লেখা নিয়ে কাজ, আর মেমরির ভেতরে আসলে কী ঘটে — pointer, মালিকানা আর lifetime. আজকের Student আর Order struct সেখানে বারবার ফিরে আসবে, এবার শত শতটি একসাথে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১enum তো C থেকেই ছিল — C++11 কেন পুরোনোটা ঠিক না করে একটি নতুন enum class যোগ করল? আর নাম কেন “enum class”?
পুরোনো enum-এর সমস্যাগুলো বহু বছর ধরে সবার জানা ছিল। নাম বাইরে ছড়িয়ে পড়ে, তাই বড় প্রজেক্টে দুটি লাইব্রেরির Red বা None নিয়ে সংঘর্ষ হতো, আর সবাই COLOR_RED-এর মতো উপসর্গ জুড়ে বাঁচতেন। নিঃশব্দে int হয়ে যাওয়ায় দুটি সম্পূর্ণ ভিন্ন enum-এর তুলনা বা যোগ compile হয়ে যেত। আর তৃতীয় একটি কম-আলোচিত সমস্যা ছিল: plain enum-এর underlying type standard ঠিক করে দিত না, compiler তালিকার মানগুলো দেখে বেছে নিত। ফলে পুরো তালিকা না দেখে enum-এর আকার জানা যেত না, তাই আগে থেকে শুধু ঘোষণা (forward declaration) করাও যেত না — একটি enum-এ নতুন মান যোগ করলে সেটি যত ফাইলে include হয়েছে, সব নতুন করে compile করতে হতো।
২০০০-এর দশকের মাঝামাঝি David E. Miller, Herb Sutter আর Bjarne Stroustrup “strongly typed enums” নামে প্রস্তাব নিয়ে কাজ করেন, যা C++11-এ গৃহীত হয়। প্রশ্ন ছিল — পুরোনো enum-এর আচরণই বদলে দেওয়া যায় না কেন? উত্তর সেই পুরোনো C++ নীতি: চালু কোড ভাঙা যাবে না. পৃথিবীর কোটি কোটি লাইন কোড plain enum-এর নাম সরাসরি ব্যবহার করে, if (color == 0) লেখে, enum দিয়ে array-এর সূচক বানায়। আচরণ বদলালে এর সব রাতারাতি ভেঙে যেত, আর C-এর সাথে সামঞ্জস্যও হারাত। তাই সিদ্ধান্ত হলো: পুরোনোটা যেমন আছে থাক, পাশে নতুন নিরাপদ রূপ আসুক — নতুন কোড নতুনটা বেছে নেবে।
তবে পুরোনো enum-কেও কিছু উপহার দেওয়া হয়: C++11 থেকে plain enum-এও enum Color : int লিখে underlying type ঠিক করা যায়, আর Color::Red লিখে পুরো নামেও ডাকা যায় (যদিও নাম বাইরে ছড়ানো বন্ধ হয় না)।
আর নাম? নতুন keyword (যেমন scoped_enum) আনলে সেই নামের সব ভ্যারিয়েবল ভেঙে যেত — পর্ব ০৫-এ auto-র গল্পে একই হিসাব দেখেছেন। তাই দুটি পুরোনো keyword জোড়া লাগানো হলো: enum class (বা সমার্থক enum struct). এতে কোনো পুরোনো কোড ভাঙে না, কারণ আগে এই দুই শব্দ পাশাপাশি বসানো অবৈধ ছিল। নামটি একটু বিভ্রান্তিকর — এটি আসলে class নয়, এর ভেতরে ফাংশন রাখা যায় না — কিন্তু “class” শব্দটি বোঝায় নিজের scope আর কঠোর টাইপ, যা class-এর মতোই।
মূল উপলব্ধি: একটি জনপ্রিয় ভাষা ভুল শুধরাতে পারে খুব কম পথে — বেশিরভাগ সময় পুরোনোটা রেখে পাশে ভালোটা যোগ করে। তাই C++-এ প্রায় প্রতিটি জিনিসের “পুরোনো” আর “আধুনিক” রূপ পাশাপাশি থাকে, আর কোনটা বেছে নেবেন সেটা জানাই আধুনিক C++ জানার বড় অংশ।
প্র ০২C++-এ struct আর class যদি প্রায় একই হয়, তাহলে দুটো শব্দ রাখা হলো কেন? আর বাস্তব প্রজেক্টে কোনটা কখন লেখা হয়?
ইতিহাসটা শুরু হয় ১৯৭৯-৮০ সালে, যখন Bjarne Stroustrup “C with Classes” বানাচ্ছিলেন। তাঁর লক্ষ্য ছিল C-এর ওপর নতুন ক্ষমতা যোগ করা, কিন্তু প্রতিটি বৈধ C প্রোগ্রাম যেন C++-এও চলে। C-এর struct তাই রাখতেই হতো। অন্যদিকে তিনি চাইছিলেন এমন একটি নতুন ধারণা — যেখানে ডেটা লুকানো থাকে, শুধু নির্দিষ্ট ফাংশন দিয়ে ছোঁয়া যায় (Simula ভাষা থেকে অনুপ্রাণিত)। সেজন্য এল class, যার ডিফল্ট private.
দুটো সম্পূর্ণ আলাদা যন্ত্র বানালে ভাষা জটিল হতো, আর নিয়ম দাঁড়াত অদ্ভুত: “struct-এ ফাংশন রাখা যাবে না” বা “class aggregate হতে পারবে না।” Stroustrup বরং দুটোকে একই যন্ত্র বানালেন, শুধু ডিফল্ট আলাদা রাখলেন। C-এর struct-এর সব member public ছিল — সেই আচরণ অক্ষুণ্ণ রইল। নতুন class-এ ডিফল্ট private — কারণ লুকানোই ছিল তার উদ্দেশ্য।
ভাষা দুটোকে এক বললেও সম্প্রদায় একটি স্পষ্ট রীতি গড়ে তুলেছে। C++ Core Guidelines (Stroustrup আর Herb Sutter-এর নেতৃত্বে লেখা) পরামর্শ দেয়: টাইপের যদি কোনো invariant থাকে — এমন নিয়ম, যা সবসময় সত্য থাকতে হবে — তাহলে class; আর member-গুলো যদি স্বাধীনভাবে যেকোনো মান নিতে পারে, তাহলে struct. আরও বলে, কোনো member private হলে class লিখুন। Google-এর C++ style guide প্রায় একই কথা বলে: struct শুধু এমন নিষ্ক্রিয় বস্তুর জন্য, যা কেবল ডেটা বহন করে।
একটা উদাহরণে পার্থক্যটা পরিষ্কার হয়। একটি Point { x, y }-এ যেকোনো x আর যেকোনো y বৈধ — struct. একটি Date-এ মাস ১-১২-এর বাইরে বা ফেব্রুয়ারির ৩০ তারিখ অবৈধ — কেউ সরাসরি d.day = 30 লিখতে পারলে নিয়ম ভাঙবে, তাই ডেটা private রেখে যাচাই করা constructor দরকার — class. ফলে কোডে struct শব্দটি পাঠককে একটি বার্তা দেয়: “এটা সরল ডেটা, ভেতরে কোনো গোপন নিয়ম নেই, নির্ভয়ে ছুঁতে পারেন।”
একটি ছোট সূক্ষ্মতা: একই টাইপ এক জায়গায় struct S; আর অন্য জায়গায় class S; বলে ঘোষণা করা বৈধ, কিন্তু MSVC এতে সতর্ক করে, কারণ তার name mangling-এ (পর্ব ০৩) শব্দটির প্রভাব আছে। তাই একটি টাইপের জন্য সবখানে একই শব্দ ব্যবহার করুন।
মূল উপলব্ধি: ভাষার নিয়ম আর দলের রীতি দুটো আলাদা স্তর। compiler-এর কাছে struct আর class প্রায় এক, কিন্তু মানুষের কাছে শব্দটিই একটি নকশার ঘোষণা। ভালো কোড compiler আর মানুষ — দুজনের জন্যই লেখা হয়।
প্র ০৩Designated initializer C ভাষায় এসেছিল ১৯৯৯ সালে, C++-এ এল ২০২০ সালে — দুই দশক দেরি কেন? আর C++ কেন ক্রম মানার কড়া নিয়ম দিল, যা C-তে নেই?
C99 standard-এ designated initializer আসে খুবই উদার রূপে: যেকোনো ক্রমে লেখা যায় ({.y = 2, .x = 1}), একই member দুবার লেখা যায় (শেষেরটি জেতে), ভেতরের struct-এর member সরাসরি (.pos.x = 1), array-এর নির্দিষ্ট ঘর ([5] = 10), আর নামসহ ও নামছাড়া মান মেশানো — সবই চলে। C-তে এতে ঝুঁকি কম, কারণ C-এর struct শুধু নিষ্ক্রিয় বাইটের সমষ্টি; কোন member কখন ভরল, তাতে কিছু আসে যায় না।
C++-এ ব্যাপারটি জটিল। C++-এর member হতে পারে std::string-এর মতো টাইপ, যার constructor আছে — যা মেমরি নেয়, ফাইল খোলে, এমনকি exception ছুড়তে পারে। ভাষার একটি মৌলিক প্রতিশ্রুতি হলো: member-রা সবসময় ঘোষণার ক্রমে তৈরি হয়, আর উল্টো ক্রমে ধ্বংস হয়. এখন ধরুন {.b = makeB(), .a = makeA()} লেখা গেল। পাঠক ভাববেন makeB() আগে চলবে, কিন্তু তৈরি হবে আগে a. মাঝপথে exception হলে কোনটা তৈরি হয়ে গেছে আর কোনটা ধ্বংস করতে হবে — হিসাব গুলিয়ে যায়। কমিটি এমন বিভ্রান্তি তৈরি করতে চায়নি। এছাড়া constructor-এর overload resolution, initializer_list আর aggregate-এর নিয়মের সাথে কীভাবে মেলাবে — সেই নকশাও দীর্ঘদিন ঠিক হয়নি।
এর মধ্যে GCC আর Clang C++ কোডেও এটাকে নিজস্ব extension হিসেবে কিছুটা সমর্থন দিত, কারণ Linux kernel-এর মতো C কোডের সাথে কাজ করা প্রোগ্রামাররা এটা খুব চাইতেন। শেষে Tim Shen, Richard Smith ও অন্যদের প্রস্তাব (P0329) একটি আপস নিয়ে আসে: designated initializer আসবে, কিন্তু শুধু ঘোষণার ক্রমে, মেশানো ছাড়া, সরাসরি nested (.pos.x) বা array-designator ছাড়া। এতে লেখার ক্রম আর তৈরির ক্রম সবসময় এক থাকে। প্রস্তাবটি ২০১৭ সালে গৃহীত হয়ে C++20-এ যুক্ত হয়। ভেতরের struct-এ নাম দিতে চাইলে brace নেস্ট করে লেখা যায়: {.pos = {.x = 1, .y = 2}}.
এই কড়া নিয়মের একটি বাড়তি লাভও আছে: কেউ struct-এ member-দের ক্রম বদলালে পুরোনো designated initializer-গুলো compile error দেয়, ফলে চুপচাপ ভুল মান বসে না। অবস্থানভিত্তিক {"Biryani", 2, 250.0} লেখায় এই সুরক্ষা নেই — দুটি int member-এর জায়গা বদলালে compiler কিছুই টের পায় না।
মূল উপলব্ধি: একই বৈশিষ্ট্য দুই ভাষায় দুই রূপ নেয়, কারণ ভাষা দুটির প্রতিশ্রুতি আলাদা। C++ যখন C থেকে কিছু নেয়, তখন নিজের নিয়মের (constructor, ধ্বংসের ক্রম, exception) সাথে মিলিয়ে নেয় — আর তাতে সময় লাগে।
প্র ০৪std::tuple এত নমনীয়, তবু অভিজ্ঞ প্রোগ্রামাররা ফেরত-মানে প্রায়ই নাম দেওয়া struct চান কেন? Structured binding কি এই সমস্যা পুরো মিটিয়ে দেয়?
std::pair C++98-এর STL থেকেই আছে, কারণ std::map-এর key-value জোড়া রাখতে একটি সাধারণ টাইপ দরকার ছিল। std::tuple আসে C++11-এ, Boost লাইব্রেরির Boost.Tuple (Jaakko Järvi-র কাজ) থেকে। Template-এর জগতে এরা অমূল্য: যে কোড আগে থেকে জানে না কয়টি, কোন টাইপের মান আসবে, সেখানে নাম দেওয়া সম্ভবই নয় — std::tuple সেখানে একমাত্র উপায়।
কিন্তু সাধারণ ব্যবসায়িক কোডে নামহীনতা দাম চায়। সবচেয়ে বিখ্যাত উদাহরণ standard library নিজেই: std::map::insert ফেরত দেয় std::pair<iterator, bool>. বহু বছর ধরে কোডে if (result.second) দেখা যেত — নতুন পাঠক বুঝতেন না second মানে “নতুন উপাদান যোগ হয়েছে কি না।” আরও বিপজ্জনক হলো একই টাইপের দুটি মান: std::pair<int, int>-এ min আর max উল্টে ফেললেও compiler কিছু বলবে না, কারণ দুটোই int. নাম দেওয়া struct-এ r.min লিখে ভুল করা কঠিন।
C++17-এর structured binding (Herb Sutter, Bjarne Stroustrup আর Gabriel Dos Reis-এর প্রস্তাব) ডাকার জায়গায় নাম দিয়ে অনেকটা আরাম দেয়: auto [it, inserted] = m.insert(...); — এখন inserted পড়তে সহজ। কিন্তু খেয়াল করুন, নামগুলো দেয় ডাকার জায়গার প্রোগ্রামার, আর বাঁধা হয় শুধু ক্রম দিয়ে। কেউ auto [hi, lo] = findMinMax(v); লিখলে সব compile হবে, শুধু উত্তর উল্টো। Struct হলে অন্তত ঘোষণায় min আর max লেখা থাকে, যা IDE-তে দেখা যায়, আর member-এর নাম দিয়ে পড়লে ক্রমের ভুল অসম্ভব। তাই structured binding সমস্যা কমায়, মেটায় না।
Standard কমিটি নিজেও এই শিক্ষা নিয়েছে। C++20-এর ranges algorithm-গুলো ফল দেয় নাম দেওয়া struct-এ — std::ranges::minmax দেয় min আর max member-ওয়ালা ফল, std::ranges::copy দেয় in আর out member-ওয়ালা ফল। আর নাম দেওয়া struct হারায় না কিছুই: structured binding struct-এর ওপরেও চলে।
একটি বাস্তব নিয়ম: এক ফাংশনের ভেতরে অল্প সময়ের জন্য, বা generic কোডে, pair/tuple ঠিক আছে। কিন্তু ফাংশনের ঘোষণায়, ফাইলের বাইরে যায় এমন API-তে, বা তিন বা তার বেশি মানে — struct লিখুন।
মূল উপলব্ধি: নাম হলো সবচেয়ে সস্তা documentation, যা compiler-ও পড়ে। কোডের সংক্ষিপ্ততা লেখার সময় বাঁচায়; কিন্তু কোড লেখার চেয়ে পড়া হয় অনেক বেশি বার — তাই পাঠকের সময় বাঁচানোই আসল লাভ।
প্র ০৫“Data-oriented design”-এর সমর্থকেরা বলেন, struct কীভাবে সাজাচ্ছেন তার ওপর প্রোগ্রামের গতি কয়েক গুণ বদলে যেতে পারে। member-এর ক্রম বা struct-এর আকার কেন এত গুরুত্বপূর্ণ?
আধুনিক CPU মেমরি থেকে একটি-একটি বাইট আনে না, আনে একসাথে একটি টুকরো — cache line, সাধারণ x86 ও ARM চিপে ৬৪ বাইট। RAM থেকে আনা খুব ধীর (CPU-র হিসাবের তুলনায় শত গুণ পর্যন্ত), কিন্তু cache-এ থাকা ডেটা প্রায় বিনামূল্যে পড়া যায়। তাই প্রশ্নটা দাঁড়ায়: আপনি যে ডেটা দরকার, সেটা কি পাশাপাশি আছে, যাতে প্রতিটি cache line-এর পুরোটা কাজে লাগে?
একটি গেমে দশ লাখ কণার (particle) কথা ভাবুন। প্রতিটির struct-এ আছে অবস্থান (x, y, z), বেগ, রং, আকার, বয়স, নাম… ধরুন ৬৪ বাইট। প্রতি frame-এ শুধু অবস্থান হালনাগাদ করতে হয় — অর্থাৎ ১২ বাইট। কিন্তু struct-গুলো পাশাপাশি রাখা (array of structs, AoS) হলে CPU প্রতিটি কণার জন্য পুরো ৬৪ বাইটের line আনে, যার ৫২ বাইট এই কাজে অপ্রয়োজনীয়। বিকল্প হলো struct of arrays (SoA): সব x এক সারিতে, সব y আরেক সারিতে। এখন প্রতিটি cache line-এর পুরোটাই কাজের ডেটা, আর CPU একসাথে কয়েকটি সংখ্যায় হিসাব (SIMD) করতেও পারে। বাস্তবে এমন পরিবর্তনে বহুগুণ গতি পাওয়ার উদাহরণ প্রচুর।
এই চিন্তাধারাকে বলে data-oriented design. গেম ইঞ্জিনের জগতে এটি বিশেষ জনপ্রিয় হয় Mike Acton-এর CppCon 2014-এর বিখ্যাত বক্তৃতা “Data-Oriented Design and C++”-এর পর, যেখানে তিনি বলেন প্রোগ্রামের আসল কাজ ডেটাকে এক রূপ থেকে আরেক রূপে বদলানো, তাই নকশা শুরু হওয়া উচিত ডেটা থেকে। অনেক আধুনিক গেম ইঞ্জিনের Entity Component System এই ধারণার ওপর দাঁড়িয়ে।
ছোট পরিসরেও একই নিয়ম খাটে। Padding-এর কারণে ভুল ক্রমের struct বড় হয় — {char, int, char} ১২ বাইট, {int, char, char} ৮ বাইট। দশ লাখ অবজেক্টে চার মেগাবাইটের পার্থক্য, আর তার মানে আরও কম অবজেক্ট cache-এ ধরে। আরেকটি কৌশল “hot/cold splitting”: প্রায়ই পড়া member এক struct-এ, কালেভদ্রে পড়া member (যেমন বিবরণের লেখা) আলাদা জায়গায়।
তবে সাবধান: এগুলো সব জায়গায় খাটানো উচিত নয়। SoA কোড পড়তে কঠিন, আর দশটি অবজেক্টের প্রোগ্রামে কোনো পার্থক্যই হবে না। নিয়ম হলো — আগে পরিষ্কার struct লিখুন, তারপর মেপে দেখুন (পর্ব ৪৯-এর profiler) কোথায় সময় যাচ্ছে, তারপর শুধু সেখানে ডেটার রূপ বদলান।
মূল উপলব্ধি: struct শুধু একটি যৌক্তিক ধারণা নয়, মেমরিতে বাইটের একটি বাস্তব নকশাও। C++ আপনাকে এই নকশা নিজের হাতে ঠিক করতে দেয় — এটাই ভাষাটির গতির উৎস, আর বড় সিস্টেমে এটাই প্রোগ্রামারের দায়িত্ব।
অনুশীলন
বেশিরভাগ কাজে কোড লিখতে হবে না — আগে অনুমান করুন, তারপর উত্তর মিলিয়ে নিন।
-
টাইপের নকশা: একটি মোবাইল ওয়ালেটের লেনদেনের তথ্য রাখতে হবে — ধরন (Send Money, Cash Out, Payment, Recharge), টাকার পরিমাণ, প্রাপকের নম্বর আর সফল হয়েছে কি না। কোনটা
enum class, কোনটা struct-এর member হবে, আর প্রতিটির টাইপ ও ডিফল্ট মান কী দেবেন — কাগজে লিখুন।ধরন নির্দিষ্ট কয়েকটির একটি, তাই
enum class TxnType : std::uint8_t { SendMoney, CashOut, Payment, Recharge };. লেনদেন struct:struct Transaction { TxnType type{TxnType::SendMoney}; std::int64_t amountPoisha{}; std::string receiver; bool success{false}; };. টাকা পয়সায় পূর্ণসংখ্যা (পর্ব ০৫-এর কারণে), ফোন নম্বরstd::string— কারণ শুরুর 0 সংখ্যায় হারিয়ে যায় আর নম্বর দিয়ে কেউ যোগ-বিয়োগ করে না। প্রতিটি member-এর ডিফল্ট মান আছে, তাইTransaction t;-ও নিরাপদ। এটা struct, কারণ member-গুলো স্বাধীন; কিন্তু যদি নিয়ম থাকত “পরিমাণ কখনো ঋণাত্মক নয়”, তাহলে class ভাবতে হতো (পর্ব ২০)। -
কোনটা compile হবে?
struct P { int x{}; int y{}; int z{}; };দেওয়া আছে। প্রতিটির জন্য বলুন compile হবে কি না, হলে x, y, z কত — (ক)P a{1, 2};(খ)P b{.y = 5};(গ)P c{.z = 3, .x = 1};(ঘ)P d{.x = 1, 2};(ঙ)auto [p, q] = P{};(ক) হবে: x = 1, y = 2, z = 0 (ডিফল্ট মান থেকে)। (খ) হবে: x = 0, y = 5, z = 0. (গ) হবে না — designator-গুলো ঘোষণার ক্রমে নেই (z আগে, x পরে)। (ঘ) হবে না — নামসহ আর নামছাড়া মান মেশানো নিষেধ। (ঙ) হবে না —
P-তে তিনটি data member, অথচ দুটি নাম; structured binding-এ নামের সংখ্যা ঠিক মিলতে হয়। -
বাগ শিকার: একটি AI কনটেস্টের সমাধানে লিখেছে —
vector<pair<pair<int,int>,int>> v; // (roll, marks), classআর পরেsortকরেv[0].first.firstছাপিয়েছে সর্বোচ্চ নম্বর পাওয়া ছাত্রের রোল ভেবে। কী কী সমস্যা দেখেন? কীভাবে নতুন করে লিখবেন?(১) পড়া প্রায় অসম্ভব —
.first.firstরোল না নম্বর, comment না দেখে বোঝা যায় না, আর comment ভুল হলেও কেউ টের পাবে না। (২)pair-এর ডিফল্ট sort আগেfirstদিয়ে, অর্থাৎ রোল দিয়ে, ছোট থেকে বড় সাজায় — তাইv[0]সবচেয়ে ছোট রোলের ছাত্র, সর্বোচ্চ নম্বরের নয়। নামহীন জোড়ায় এমন ভুল সহজেই লুকিয়ে থাকে। নতুন রূপ:struct Result { int roll{}; int marks{}; int section{}; };, তারপর নম্বর দিয়ে বড় থেকে ছোট সাজাতে একটি তুলনার নিয়ম দেওয়া (পর্ব ৩৩-৩৪-এsortআর lambda শিখবেন)। পড়ার সময়top.roll— কোনো সন্দেহ নেই। -
নিজে লিখুন (কোড): উপরের
student_minmax.cppবদলেMinMax-এ একটি তৃতীয় memberdouble average{}যোগ করুন,findMinMax-এ গড়ও হিসাব করুন, আরmain-এauto [lo, hi, avg] = ...দিয়ে তিনটিই ছাপান। তারপর ইচ্ছা করেauto [lo, hi] = ...রেখে দিয়ে compiler-এর বার্তা পড়ুন।struct:
struct MinMax { int min; int max; double average{}; };. ফাংশনে একটিint sum{};নিয়ে লুপে যোগ করুন, শেষেr.average = static_cast<double>(sum) / v.size();(পর্ব ০৫-এর integer division-এর ফাঁদ মনে রাখুন)।MinMax r{v[0], v[0]};লাইনে এখন average ডিফল্ট মান 0.0 পায়, তাই-Wextraসতর্কবার্তা দেয় না — এটাই ডিফল্ট মান দেওয়ার সুবিধা। নম্বর {72, 85, 39, 91, 64} হলে গড় 70.2. দুটি নাম রেখে দিলে GCC বলবেonly 2 names provided for structured bindingআর জানাবে'MinMax' decomposes into 3 elements— struct বদলালে ডাকার জায়গাগুলো compiler নিজেই খুঁজে দেয়।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমখাবার-ডেলিভারি অ্যাপের enum class Status-এর মানগুলোকে একটি অর্ডারের স্বাভাবিক যাত্রার ক্রমে সাজান (▲▼ চেপে সরান)
Status::Placed— গ্রাহক অর্ডার দিলেনStatus::Cooking— রেস্তোরাঁ রান্না করছেStatus::OnTheWay— রাইডার খাবার নিয়ে রওনা দিয়েছেনStatus::Delivered— খাবার গ্রাহকের হাতে পৌঁছেছে
C++-এ একই scope-এ enum Order { Pending, Done }; আর enum Payment { Pending, Paid }; লিখলে কী হয়?
- দুটি Pending আলাদা থাকে, কোনো সমস্যা নেই
- দ্বিতীয় Pending-এর নাম নিজে থেকে Pending2 হয়ে যায়
- Compile error — Pending নামটি দুবার ঘোষণা হয়েছে
- Compile হয়, কিন্তু চালানোর সময় crash করে
enum class Color { Red, Green }; থাকলে int x = Color::Green; লিখলে কী হয়?
- x-এ 1 বসে
- Compile error — enum class থেকে int-এ নিজে থেকে যাওয়া যায় না
- x-এ "Green" লেখাটি বসে
- x-এ 0 বসে
C++20-এ struct P { int x{}; int y{}; }; থাকলে নিচের কোনটি compile হয় না?
- P a{1, 2};
- P b{.y = 2};
- P c{.x = 1, .y = 2};
- P d{.y = 2, .x = 1};
C++-এ struct আর class-এর মধ্যে ভাষাগত পার্থক্য কোনটি?
- ডিফল্ট access — struct-এ public, class-এ private
- struct-এ ফাংশন রাখা যায় না, class-এ যায়
- struct-এর constructor থাকতে পারে না
- struct দ্রুত চলে, class ধীর
Student s{.name = "Rafi", .marks = 85}; এর পরে auto [n, m] = s; m = 90; চালালে s.marks-এর মান কত হয়?
- 90
- 0
- 85
- Compile error, কারণ m বদলানো যায় না
enum class Status { Placed, Cooking }; — নিজে কিছু না বলে দিলে Status-এর underlying type কী?
- std::uint8_t, কারণ মান মাত্র দুটি
- int
- unsigned int
- compiler যা খুশি বেছে নেয়
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- নিজের টাইপ = নিয়ম + অর্থ। “সংখ্যার কোড” আর নামহীন জোড়ার বদলে নাম দেওয়া টাইপ লিখলে পাঠক বোঝেন, আর compiler ভুল আটকায়।
- নতুন কোডে
enum class: নাম নিজের scope-এ, নিজে থেকেintহয় না; সংখ্যা দরকার হলেstatic_castবা C++23-এরstd::to_underlying. - enum-এর ওপর switch-এ সব case,
defaultছাড়া — নতুন মান যোগ হলে-Wswitchপ্রতিটি জায়গা দেখিয়ে দেয়। - struct-এর প্রতিটি member-কে ডিফল্ট মান দিন, আর বানান
{.name = ..., .marks = ...}দিয়ে — ঘোষণার ক্রমে, না মিশিয়ে। auto [a, b] = x;কপি বানায়; বদলাতেauto&, শুধু পড়তেconst auto&. তুলনা দরকার হলে C++20-এর= default.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১৩ · vector ও array — পাশাপাশি সাজানো ডেটাপরবর্তী পর্বমডিউল ৩-এর শুরু: একটি নয়, শত শত struct একসাথে।
- C++ প্রোগ্রামিং · কোর্স হোম৫৫টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- C প্রোগ্রামিং · struct ও unionগভীরে যানমেমরিতে struct-এর ছবি, alignment, padding, union আর bit field.