এই পর্বে যা শিখবেন
- magic value (-1, খালি string, nullptr) কেন বিপজ্জনক, আর “নেই” কথাটি টাইপে লেখার মানে কী
std::optionalতৈরি, যাচাই আর পড়ার নিরাপদ ও অনিরাপদ উপায়, আর C++23-এর monadic ফাংশনstd::variant-এর ভেতরের গঠন: একটি জায়গা, একটি লেবেল;index,get,get_if,holds_alternativestd::visitআর overloaded lambda দিয়ে প্রতিটি টাইপ সামলানো, আর নতুন টাইপ যোগে compiler কেন error দেয়- variant বনাম virtual (বন্ধ বনাম খোলা টাইপ-তালিকা),
std::any-এর সঠিক জায়গা, আর AI-এর কোডে এসব ভুল ধরা
১ · সমস্যাটা আগে — -1, খালি string আর nullptr: যে উত্তর টাইপে লেখা নেই
ধরুন একটি স্কুলের ফলাফল-সফটওয়্যারে একটি ফাংশন আছে: রোল নম্বর দিলে ছাত্রের প্রাপ্ত নম্বর ফেরত দেয়। প্রশ্ন হলো, ওই রোলের কোনো ছাত্র না থাকলে কী ফেরত দেবে? বহু বছর ধরে প্রোগ্রামাররা এর একটি সহজ উত্তর দিয়ে এসেছেন: একটি বিশেষ মান, যা “আসল” উত্তর হতে পারে না।
int marksOf(int roll); // না পেলে -1 ফেরত দেয় (কোথাও লেখা নেই!)
std::string phoneOf(int roll); // না পেলে "" (খালি string)
Student* findStudent(int roll); // না পেলে nullptr
int m = marksOf(42);
int total = m + bonus; // m যদি -1 হয়? কেউ আটকাবে নাএমন বিশেষ মানকে বলে sentinel value বা magic number. এর তিনটি গভীর সমস্যা আছে। এক, চুক্তিটি থাকে শুধু মানুষের মাথায় বা কোনো মন্তব্যে — int টাইপ দেখে কেউ বুঝতে পারে না যে -1 মানে “নেই”। দুই, যাচাই করা ঐচ্ছিক। ডাকার মানুষ if (m == -1) লিখতে ভুলে গেলে প্রোগ্রাম -1-কেই নম্বর ধরে হিসাব চালিয়ে যায়, আর ছাত্রের মোট নম্বর এক কম দেখায়। compiler-এর চোখে সব বৈধ। তিন, একদিন হয়তো -1 নিজেই বৈধ মান হয়ে যায় — যেমন নেতিবাচক মার্কিংয়ের পরীক্ষায় কারও প্রাপ্ত নম্বর সত্যিই -1. তখন “নেই” আর “-1 পেয়েছে” আলাদা করার কোনো উপায় থাকে না।
pointer-এর nullptr একই রোগের আরেক রূপ। Student* টাইপ বলে না, nullptr আসতে পারে কি না। যাচাই না করে -> লিখলে null dereference — undefined behavior, প্রায়ই crash. ALGOL W ভাষায় ১৯৬৫ সালে null reference যোগ করা Tony Hoare নিজে ২০০৯ সালের একটি বক্তৃতায় একে বলেছিলেন তাঁর “বিলিয়ন ডলারের ভুল” (ভাবনার প্রশ্ন ১-এ বিস্তারিত)।
রেলস্টেশনের কাউন্টারে টিকিট চাইলেন। কর্মচারী একটি কাগজ দিলেন, তাতে লেখা “সিট ০”। আপনি জানেন না, ০ মানে কি “সিট নেই”, নাকি সত্যিই ০ নম্বর সিট আছে। এবার ভাবুন আরেকটি কাউন্টার, যেখানে উত্তর আসে দুই রঙের খামে: সবুজ খামের ভেতরে সিট নম্বর, লাল খামে লেখা “টিকিট নেই”। খাম না খুলে, রং না দেখে আপনি সিট নম্বর পড়তেই পারবেন না। “নেই” সম্ভাবনাটি খামের গায়েই লেখা — এটাই std::optional.
এই পর্বের তিনটি টাইপ একই দর্শন থেকে এসেছে: যে কথা সত্যি, তা টাইপে লিখুন। ফলাফল “হয়তো আছে, হয়তো নেই” হলে std::optional. ফলাফল “কয়েকটি নির্দিষ্ট ধরনের যেকোনো একটি” হলে std::variant. আর সত্যিই “যেকোনো টাইপ” হলে std::any. তিনটিই C++17-এ standard library-তে এসেছে, আর তিনটিরই উদ্দেশ্য এক — ডাকার মানুষকে প্রতিটি সম্ভাবনা সামলাতে বাধ্য করা, যাতে ভুলে যাওয়া আর নীরবে না ঘটে।
এখানে লক্ষ্য type safety-কে এক ধাপ এগিয়ে নেওয়া। শুধু “এটি int” নয়, বরং “এটি হয় একটি int, নয়তো কিছুই না” — আর সেই দ্বিতীয় সম্ভাবনা সামলানো ছাড়া মানটি হাতে পাওয়ার কোনো সোজা রাস্তা রাখা হয় না। ভুলটা চলার সময় খুঁজে বের করার বদলে compile-এর সময় বা অন্তত স্পষ্ট একটি exception হিসেবে সামনে আসে।
২ · std::optional — “হয়তো একটি মান”
std::optional<T> হলো একটি ছোট বাক্স, যার দুটি অবস্থা: ভেতরে একটি T আছে, অথবা বাক্স খালি। খালি অবস্থার নাম std::nullopt. বাক্সটি একটি সাধারণ মান — heap থেকে মেমরি নেয় না, ভেতরের T আর একটি ছোট “আছে কি নেই” চিহ্ন পাশাপাশি রাখে। GCC-তে std::optional<int>-এর মাপ সাধারণত ৮ বাইট: int-এর ৪, চিহ্নের ১, বাকিটা alignment-এর ফাঁকা জায়গা।
#include <optional>
std::optional<int> a = 42; // ভেতরে 42
std::optional<int> b; // খালি
std::optional<int> c = std::nullopt; // খালি — স্পষ্ট করে লেখা
if (a) { /* মান আছে */ } // bool-এ রূপান্তর
if (b.has_value()) { /* ... */ } // একই কাজ, নাম দিয়ে
a.reset(); // a এখন খালি
a = 7; // আবার মান আছে
a.emplace(9); // ভেতরে সরাসরি তৈরিif (a) কাজ করে কারণ optional-এ একটি explicit operator bool আছে — শর্তের ভেতরে লিখলে নিজে থেকে “মান আছে কি?” প্রশ্নে বদলে যায়। আর has_value() একই প্রশ্ন, শুধু নাম দিয়ে লেখা। কোনটি লিখবেন তা রুচির ব্যাপার; দুটিই সমান দ্রুত।
মান পড়ার তিন উপায় — নিরাপত্তা আলাদা
বাক্স থেকে মান বের করার তিনটি পথ আছে, আর এদের পার্থক্য বোঝা এই পর্বের সবচেয়ে কাজের অংশগুলোর একটি। একটি ভরা optional (a, ভেতরে 42) আর একটি খালি optional (b) পাশাপাশি রেখে ধাপে ধাপে দেখুন। প্রতিটি ধাপে আগে নিজে অনুমান করুন, খালি b-এর বেলায় কী ঘটবে:
1std::optional<int> a = 42, b; // b খালি2*a; *b; // যাচাই নেই3a.value(); b.value(); // যাচাই করে4a.value_or(0); b.value_or(0); // বিকল্প মান
| কী করছি | |
| a (ভেতরে 42) | |
| b (খালি) | |
| খালি হলে কে ধরবে |
- দুই অবস্থা।
a-তে 42 আছে,bতৈরি হয়েছে কিছু না দিয়ে, তাই খালি।has_value()(বাif (opt)) এই প্রশ্নের উত্তর দেয়। নিরাপদ কোডের প্রথম ধাপ সবসময় এই প্রশ্ন — অথবা এমন একটি পড়ার উপায় বেছে নেওয়া, যা প্রশ্নটি নিজেই করে। *opt— দ্রুততম, অন্ধ।*aআরa->কোনো যাচাই ছাড়া ভেতরের মান দেয়।a-তে এটি একদম ঠিক। কিন্তু খালিb-তে*bহলো undefined behavior — standard কোনো নিশ্চয়তা দেয় না; হয়তো আবর্জনা মান আসবে, হয়তো আগের কোনো পুরোনো মান, হয়তো অন্য কিছু। এটি চালিয়ে দেখার কোড নয়।*optলিখুন শুধু তখনই, যখন ঠিক আগের লাইনে মান আছে কি না যাচাই করেছেন।value()— যাচাই করে পড়া। মান থাকলে দেয়; না থাকলে ছোড়ে std::bad_optional_access exception. কেউ catch না করলে প্রোগ্রাম স্পষ্টভাবে বন্ধ হয় — নীরবে ভুল হিসাব চালিয়ে যায় না। “খালি হওয়া এখানে সত্যিই একটি ব্যতিক্রমী ভুল” — এমন জায়গায় এটি মানানসই।value_or(x)— বিকল্প হাতে রাখা। মান থাকলে মানটি, না থাকলে আপনার দেওয়া x. কোনো exception নেই, কোনো UB নেই। কনফিগারেশনের ডিফল্ট মান, অনুপস্থিত ফোন নম্বরের জায়গায় “-” — এমন জায়গায় এটিই সবচেয়ে পরিষ্কার। খেয়াল করুন: x-এর মান সবসময় আগে তৈরি হয়, তাই ভারী হিসাব হলে নিচেরor_elseভালো।
*opt আর value()-এর পার্থক্য std::vector-এর v[i] আর v.at(i)-এর মতোই: একটি যাচাই করে না, অন্যটি করে। নতুন কোডে ডিফল্ট হিসেবে if (opt) তারপর *opt, অথবা value_or বেছে নিন। যাচাই ছাড়া *opt দেখলে সেটি সন্দেহ করার মতো লাইন। (কিছু standard library-র “hardened” মোড খালি optional-এ * ধরলে প্রোগ্রাম থামিয়ে দেয়, তবে তার ওপর ভরসা করে কোড লিখবেন না।)
আসল উদাহরণ — রোল দিয়ে ছাত্র খোঁজা
এবার শুরুর সমস্যাটি optional দিয়ে লিখুন। খোঁজার ফাংশন এখন সরাসরি বলছে, “আমি হয়তো একজন Student দেব”। ডাকার মানুষ যাচাই না করে নাম পড়তে পারবেন না — অন্তত ভুলটা লুকানো থাকবে না:
#include <optional>
#include <print>
#include <string>
#include <vector>
struct Student { int roll; std::string name; int marks; };
std::optional<Student> find(const std::vector<Student>& cls, int roll) {
for (const auto& s : cls)
if (s.roll == roll) return s;
return std::nullopt; // "not found" is part of the type
}
int main() {
std::vector<Student> cls{{1, "Rahim", 78}, {2, "Karim", 64}};
for (int roll : {2, 7}) {
if (auto s = find(cls, roll)) std::println("roll {}: {} ({})", roll, s->name, s->marks);
else std::println("roll {}: not found", roll);
}
auto grade = [](const Student& s) { return s.marks >= 70 ? 'A' : 'B'; };
std::println("grade of 1: {}", find(cls, 1).transform(grade).value_or('-'));
std::println("grade of 9: {}", find(cls, 9).transform(grade).value_or('-'));
try { std::println("{}", find(cls, 9).value().name); }
catch (const std::bad_optional_access&) { std::println("value() threw bad_optional_access"); }
}আউটপুট পাঁচ লাইন: roll 2: Karim (64), roll 7: not found, grade of 1: A, grade of 9: - আর value() threw bad_optional_access. if (auto s = find(...)) একসাথে দুটি কাজ করে: optional-টি তৈরি করে, আর সাথে সাথে জিজ্ঞেস করে মান আছে কি না — s->name শুধু সেই শাখার ভেতরেই চলে। transform(grade) হলো C++23-এর ফাংশন: মান থাকলে ভেতরের Student-কে grade-এ বদলায়, খালি হলে খালিই রাখে; শেষে value_or('-') খালির জায়গায় একটি চিহ্ন বসায়। রোল ৯ নেই, তাই শেষ লাইনে value() exception ছুড়েছে আর আমরা সেটি ধরেছি। কোথাও -1 নেই, কোথাও nullptr নেই।
৩ · optional নিয়ে কাজ — ডিফল্ট মান, C++23-এর শিকল, আর কখন optional নয়
একটি কনফিগারেশন — হয়তো লেখা আছে, হয়তো নেই
একটি সার্ভারের কনফিগারেশন ফাইলে port লেখা থাকতেও পারে, না-ও পারে। না থাকলে 8080 ধরতে হবে। optional-এর সবচেয়ে সাধারণ ব্যবহার ঠিক এটি:
std::optional<std::string> lookup(const Config& cfg, std::string_view key);
std::optional<int> toInt(std::string_view s); // "80a" হলে খালি
int port = lookup(cfg, "port") // optional<string>
.and_then(toInt) // string → optional<int>; খালি হলে থেমে যায়
.value_or(8080); // কোথাও খালি হলে 8080এখানে দুটি ধাপের যেকোনোটি ব্যর্থ হতে পারে: key না থাকা, অথবা লেখাটি সংখ্যা না হওয়া। পুরোনো ধাঁচে লিখলে দুটি নেস্ট করা if লাগত। C++23 optional-এ তিনটি monadic operation যোগ করেছে, যা এই শিকল সম্ভব করে:
| ফাংশন | মান থাকলে | খালি হলে | যে ফাংশন দেবেন, সে ফেরত দেয় |
|---|---|---|---|
and_then(f) | f(মান) চালায় | খালি optional | একটি optional (নিজেও ব্যর্থ হতে পারে) |
transform(f) | f(মান)-কে নতুন optional-এ মোড়ে | খালি optional | সাধারণ মান (ব্যর্থ হয় না) |
or_else(f) | নিজেকেই ফেরত দেয় | f() চালায় | একটি optional — বিকল্প উৎস |
মনে রাখার সহজ উপায়: যে ধাপ নিজেও “নেই” বলতে পারে, সেটি and_then-এ; যে ধাপ শুধু রূপ বদলায়, সেটি transform-এ; আর “না পেলে অন্য জায়গায় খোঁজো” হলে or_else — যেমন lookup(cfg, "port").or_else([&] { return lookup(env, "PORT"); }). শিকলের যেকোনো জায়গায় খালি হলে বাকি ধাপগুলো আর চলে না, খালিটাই শেষ পর্যন্ত গড়িয়ে যায়। একই নামের ফাংশনগুলো পর্ব ৩৭-এর std::expected-এও আছে।
optional শুধু বলে “নেই” — কেন নেই, তা বলে না। toInt("80a") খালি ফেরত দিলে আপনি জানেন না, লেখাটি খালি ছিল, অক্ষর ছিল, নাকি সংখ্যা খুব বড় ছিল। যখন কারণটি জানা দরকার (ব্যবহারকারীকে বার্তা দেখাতে, লগে লিখতে), তখন C++23-এর std::expected<T, E> নিন, যা হয় মান দেয়, নয়তো একটি error. এর পুরো আলোচনা আগের পর্বে — পর্ব ৩৭ · exception আর std::expected. নিয়মটা সহজ: “না থাকাটা স্বাভাবিক” হলে optional, “ব্যর্থতার কারণ গুরুত্বপূর্ণ” হলে expected.
optional, pointer আর sentinel — কোনটি কখন
| প্রশ্ন | sentinel (-1, "") | pointer (T*) | std::optional<T> |
|---|---|---|---|
| “নেই” টাইপে লেখা? | না, শুধু মন্তব্যে | আংশিক — nullptr হতে পারে, কিন্তু বাধ্যতামূলক নয় | হ্যাঁ, স্পষ্ট |
| যাচাই ভুলে গেলে | ভুল মান নীরবে ঢোকে | UB, প্রায়ই crash | value()-এ exception; *-এ UB |
| মালিকানা | মানটি নিজের | অস্পষ্ট — কে মুছবে? | মানটি নিজের, কপি হয় |
| মেমরি | বাড়তি কিছু নেই | মানটি অন্য কোথাও থাকতে হয় | T + একটি ছোট চিহ্ন, heap নয় |
| ভালো জায়গা | পুরোনো C API-র সাথে কাজ | বড় বা শেয়ার করা object দেখানো | “হয়তো একটি মান” ফেরত দেওয়া |
pointer একেবারে বাদ নয়। কোনো বড় object আগে থেকেই কোথাও আছে আর আপনি শুধু সেটিকে দেখাতে চান — কপি না করে — তখন const Student* ফেরত দেওয়া যুক্তিসংগত (মালিকানা নেই এমন observer pointer, পর্ব ১৫ আর ১৭)। কারণ optional ভেতরে মানটির নিজের কপি রাখে।
optional<T&> কেন নেই — আর C++26-এ কী বদলাল
C++17 থেকে C++23 পর্যন্ত std::optional<int&> লেখা যায় না — compile error. বহু বছর ধরে এর কারণ ছিল একটি অমীমাংসিত প্রশ্ন: opt = x; লিখলে কি পুরোনো reference নতুন object-কে দেখাতে শুরু করবে (rebind), নাকি যে object-কে দেখাচ্ছে তার মান বদলাবে (assign through)? দুটি উত্তরই কারও কাছে “স্বাভাবিক”, আর ভুল বোঝাবুঝি মানেই বাগ। তাই এতদিন বিকল্প ছিল সাধারণ pointer, বা std::optional<std::reference_wrapper<T>>. C++26-এর জন্য একটি প্রস্তাব (P2988) গৃহীত হয়ে optional<T&> যোগ হয়েছে, rebind আচরণসহ। তবে compiler আর library-তে এর সমর্থন এখনো আসছে — এই কোর্সের C++23 পরিবেশে আপনি এখনো error পাবেন। নতুন কোডে আপাতত pointer ব্যবহার করুন।
৪ · std::variant — “এগুলোর ঠিক একটি”
optional দুটি সম্ভাবনা বোঝায়: একটি T, অথবা কিছু না। কিন্তু অনেক সময় উত্তর হয় কয়েক ধরনের — আর প্রতিটি ধরনের সাথে আলাদা তথ্য থাকে। একটি bKash বা Nagad পেমেন্টের ফল ভাবুন:
- সফল — সাথে লেনদেনের আইডি আর টাকার পরিমাণ
- প্রত্যাখ্যাত — সাথে কারণ (“ব্যালেন্স কম”, “ভুল PIN”)
- নেটওয়ার্ক সমস্যা — সাথে কত সেকেন্ড পরে আবার চেষ্টা করতে হবে
পুরোনো পথ ছিল একটি বড় struct, যাতে সব ফিল্ড আছে আর একটি kind লেবেল বলে দেয় কোনগুলো অর্থপূর্ণ। সমস্যা: সফল লেনদেনে reason ফিল্ডে কী আছে? কেউ ভুল করে সেটি পড়লে কেউ আটকাবে না। std::variant<Success, Declined, NetworkError> ঠিক এই অবস্থাটি টাইপে লেখে: এই তিনটির ঠিক একটি, একসাথে দুটি কখনো নয়, আর কোনটি তা সবসময় জানা। এমন টাইপকে তত্ত্বের ভাষায় বলে sum type, আর বাস্তবায়নের ভাষায় tagged union.
#include <variant>
std::variant<int, double, std::string> v; // শুরুতে: int, মান 0
v = 3.5; // এখন double
v = std::string("Dhaka"); // এখন string
v.index(); // 2 — তালিকার কত নম্বর টাইপ
std::holds_alternative<std::string>(v); // true
std::get<std::string>(v); // "Dhaka"
std::get<int>(v); // throw std::bad_variant_access
std::get_if<int>(&v); // nullptr — exception নয়variant-এর ভেতরে আসলে কী আছে, ধাপে ধাপে দেখুন। ছবিগুলো সাধারণ বাস্তবায়নের সরল রূপ — standard শুধু আচরণ বলে, ঠিক কোন বাইটে কী থাকবে তা বলে না:
v: [ জায়গা (সবচেয়ে বড় টাইপের মাপ) ][ tag ]
[ int: 0 ][ 0 ]
জায়গার মাপ = string-এর মাপ (সবচেয়ে বড়)
GCC 64-বিটে sizeof(v) সাধারণত 40 বাইট
১. পুরোনো int-কে ধ্বংস করা হলো ২. একই জায়গায় নতুন double তৈরি হলো ৩. tag বদলে 1 v: [ double: 3.5 ][ 1 ]
১. double ধ্বংস (কিছু করার নেই, সাধারণ সংখ্যা) ২. জায়গায় std::string তৈরি — constructor চলল ৩. tag বদলে 2 v: [ string: "Dhaka" ][ 2 ] পরে v-এর টাইপ আবার বদলালে string-এর destructor চলবে
v.index() → 2
holds_alternative<string>(v) → true
get<string>(v) → "Dhaka"
get<int>(v) → tag 0 নয়!
throw bad_variant_access
get_if<int>(&v) → nullptr
- একটি জায়গা, একটি লেবেল। variant-এর ভেতরে এমন মাপের একটি জায়গা থাকে, যাতে তালিকার সবচেয়ে বড় টাইপটিও আঁটে — সব টাইপের জন্য আলাদা জায়গা নয়। পাশে একটি ছোট সংখ্যা (tag) বলে, এই মুহূর্তে কোন টাইপ আছে। কিছু না দিয়ে বানালে প্রথম টাইপটি ডিফল্ট মান নিয়ে তৈরি হয় — এখানে int, মান 0. variant কখনো heap থেকে মেমরি নেয় না।
- টাইপ বদলানো মানে পুরোনোকে বিদায়।
v = 3.5লিখলে variant আগে int-কে ধ্বংস করে, তারপর একই জায়গায় double তৈরি করে, আর tag-এ লেখে 1. C-এর union-এ এই তিনটি কাজ আপনাকে নিজে মনে রেখে করতে হতো; এখানে variant নিজেই করে। - জটিল টাইপও নিরাপদ। string-এর মতো টাইপ, যার constructor আর destructor আছে, C-এর union-এ রাখা ঝুঁকির — কখন কোনটি ডাকতে হবে, তা compiler জানে না। variant জানে, কারণ tag দেখে। string থেকে অন্য টাইপে গেলে বা variant নিজে ধ্বংস হলে ঠিক string-এর destructor চলবে, ফলে মেমরি leak হয় না।
- পড়তে গেলে tag যাচাই হয়।
get<string>tag মিলিয়ে দেখে মান দেয়।get<int>tag-এ 0 না পেয়ে ছোড়ে std::bad_variant_access — C-এর union-এর মতো আবর্জনা বাইট পড়ে ফেলে না। আর std::get_if একটি pointer নেয়, আর ভুল টাইপে exception-এর বদলে nullptr দেয়। প্রশ্ন শুধু “এটা কি string?” হলে std::holds_alternative.
কয়েকটি নিয়ম জেনে রাখুন। এক, variant-এ reference, array বা void রাখা যায় না। দুই, একই টাইপ দুবার থাকলে (variant<int, int>) টাইপ দিয়ে get<int> লেখা যায় না, কারণ কোনটি বোঝাচ্ছেন তা অস্পষ্ট — তখন index দিয়ে get<0>. তবে এর চেয়ে ভালো হলো আলাদা নামের ছোট struct বানানো, যেমন struct Taka { int v; }; struct Poisa { int v; };. তিন, “খালি” অবস্থা চাইলে তালিকার প্রথমে std::monostate রাখুন — একটি ফাঁকা টাইপ, যার কাজই হলো “এখনো কিছু নেই” বোঝানো। প্রথম টাইপের ডিফল্ট constructor না থাকলে variant-এর ডিফল্ট constructor-ও থাকে না, তখনও monostate কাজে লাগে।
C-তে একই কাজ করতে হতো হাতে: একটি union আর তার পাশে একটি enum tag, দুটো মিলিয়ে একটি struct — আর tag ঠিক রাখার দায়িত্ব পুরোটাই প্রোগ্রামারের। tag বদলাতে ভুলে গেলে, বা ভুল সদস্য পড়লে, compiler কিছু বলত না। std::variant ঠিক সেই নকশা, কিন্তু tag-এর হিসাব, constructor-destructor ডাকা আর পড়ার সময় যাচাই — সব টাইপ-ব্যবস্থার হাতে। C-এর হাতে বানানো tagged union কেমন দেখায়, তা দেখতে C কোর্সের struct ও union পর্ব পড়ুন।
৫ · std::visit আর overloaded lambda — প্রতিটি টাইপের জন্য নিজের কোড
variant-এ কী আছে তা index() দেখে switch লিখে বের করা যায়, কিন্তু সেটি সেই পুরোনো, ভুলপ্রবণ পথ। C++-এর আসল যন্ত্র হলো std::visit: আপনি এমন একটি “ডাকা যায় এমন জিনিস” দেন, যা variant-এর প্রতিটি টাইপ নিতে পারে, আর visit চলার সময় যে টাইপ আছে, সেটি দিয়ে ঠিক সংস্করণটি ডাকে।
সবচেয়ে সহজ রূপ একটি generic lambda — auto প্যারামিটার সব টাইপই নেয়:
std::variant<int, double, std::string> v = 3.5;
std::visit([](const auto& x) { std::println("{}", x); }, v); // 3.5কিন্তু প্রায়ই প্রতিটি টাইপের জন্য আলাদা কাজ দরকার। তখন একটি ছোট কৌশল ব্যবহার করা হয়, যাকে বলে overloaded pattern. কয়েকটি lambda-কে একটি object-এ জুড়ে দেওয়া হয়, যাতে সেই object-এর ডাকার অপারেটর overload হয়ে যায় — প্রতিটি টাইপের জন্য একটি:
template<class... Ts>
struct overloaded : Ts... { using Ts::operator()...; }; // সব lambda-র operator() একসাথে
// C++17-এ এই লাইনটিও লাগত; C++20 থেকে compiler নিজেই বুঝে নেয়:
// template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;লাইনটি ছোট, কিন্তু ঘন। class... Ts একটি parameter pack — যতগুলো খুশি টাইপ (পর্ব ২৯-এর variadic template). overloaded সবগুলো lambda-র টাইপ থেকে একসাথে inherit করে। আর using Ts::operator()...; প্রতিটি base-এর operator()-কে এক জায়গায় এনে overload-এর একটি সেট বানায় (এটি একটি pack-ভিত্তিক using-declaration). ফল: একটি object, যাকে Circle দিলে Circle-এর lambda চলে, Rect দিলে Rect-এর। আশ্চর্যের বিষয়, এই দরকারি টুকরোটি এখনো standard library-তে নেই — প্রত্যেক প্রজেক্ট নিজে লিখে নেয়।
একটি আকৃতি — Circle অথবা Rect
struct Circle { double r; };
struct Rect { double w, h; };
using Shape = std::variant<Circle, Rect>;
double area(const Shape& s) {
return std::visit(overloaded{
[](const Circle& c) { return 3.14159 * c.r * c.r; },
[](const Rect& r) { return r.w * r.h; },
}, s);
}
// area(Rect{3, 4}) == 12, area(Circle{1}) ≈ 3.14159area(Rect{3, 4}) ডাকলে ভেতরে কী ঘটে, ধাপে ধাপে দেখুন। লক্ষ করুন, কাজের একটি অংশ ঘটে compile-এর সময়, আর বাকিটা চলার সময়:
- compile
টেবিলপ্রতি টাইপে একটি ঘর - tag পড়া
index() = 1চলার সময় - ঘর [1]
Rectখোঁজাখুঁজি নেই - ডাক
Rect lambdaconst Rect& - ফল
12.0double
Shape = variant<Circle, Rect>
compiler প্রতিটি টাইপের জন্য জিজ্ঞেস করে:
overloaded{...}(const Circle&) ডাকা যায়? হ্যাঁ → double
overloaded{...}(const Rect&) ডাকা যায়? হ্যাঁ → double
সব ফেরত-টাইপ এক? হ্যাঁ
=> টেবিল: [0] Circle-এর lambda [1] Rect-এর lambda
s: [ Rect: w=3, h=4 ][ tag = 1 ] visit শুধু tag পড়ে: 1
টেবিল[1] → Rect-এর lambda if-else-এর শিকল নেই, টাইপের তুলনা নেই — tag একটি সংখ্যা, সংখ্যাটিই ঘরের ঠিকানা
[](const Rect& r) { return r.w * r.h; }
r = s-এর ভেতরের Rect-এর reference
(কপি হয়নি, যাচাই আগেই হয়ে গেছে)
3 * 4 = 12 visit যা ফেরত দেয়, area() তাই ফেরত দেয়: 12.0
- compile-এর সময় একটি টেবিল। compiler variant-এর প্রতিটি টাইপ নিয়ে যাচাই করে, visitor-কে সেই টাইপ দিয়ে ডাকা যায় কি না, আর সবার ফেরত-টাইপ এক কি না। একটিও না মিললে এখানেই compile error — এটিই পরের অংশের মূল কথা। সব মিললে প্রতিটি টাইপের জন্য একটি ঘর বিশিষ্ট একটি ছোট টেবিল তৈরি হয় (প্রচলিত বাস্তবায়নে)।
- চলার সময় শুধু tag পড়া। variant-এর ভেতরে এখন Rect, tag 1. visit-কে কোনো অনুমান করতে হয় না — তথ্যটি variant নিজেই রেখেছে।
- tag দিয়ে সরাসরি ঘর। tag-টি একটি ছোট সংখ্যা, তাই টেবিলের ঘর বেছে নিতে কোনো তুলনার শিকল লাগে না। standard বলে, একটি variant-এর visit-এর খরচ টাইপের সংখ্যার ওপর নির্ভর করবে না। পর্ব ২৫-এর vtable-এর সাথে মিলটা খেয়াল করুন — পার্থক্য হলো, এখানে টেবিলটি variant-এর টাইপ-তালিকা থেকে তৈরি, object-এর ভেতরে কোনো vptr নেই।
- ঠিক টাইপের reference. Rect-এর lambda পায়
const Rect&— variant-এর ভেতরের object-টিই, কপি নয়।std::get-এর মতো আলাদা যাচাই লাগে না, কারণ ঘর বাছাইয়ের মধ্যেই টাইপ নিশ্চিত হয়ে গেছে। - ফল ফেরত। lambda 12 ফেরত দিল, visit সেটিই ফেরত দিল। Circle হলে একই পথে ঘর [0], আর ফল πr². কোডে কোথাও “এটা কি Rect?” প্রশ্ন নেই — প্রশ্নটি টাইপ-ব্যবস্থা নিজেই উত্তর দিয়েছে।
আসল উদাহরণ — পেমেন্টের ফল
এবার শুরুর পেমেন্ট-ফলের উদাহরণটি পুরো প্রোগ্রামে দেখুন। তিনটি ভিন্ন ফল একটি তালিকায়, আর একটি visit প্রতিটির জন্য ঠিক বার্তাটি ছাপায়। শেষে get_if আর holds_alternative-ও দেখানো হলো:
#include <print>
#include <string>
#include <variant>
#include <vector>
struct Success { std::string txn; int taka; };
struct Declined { std::string reason; };
struct NetworkError { int retryAfter; };
using PayResult = std::variant<Success, Declined, NetworkError>;
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
int main() {
std::vector<PayResult> all{Success{"TX7781", 500}, Declined{"low balance"}, NetworkError{30}};
for (const auto& r : all)
std::visit(overloaded{
[&](const Success& s) { std::println("[{}] paid {} taka, id {}", r.index(), s.taka, s.txn); },
[&](const Declined& d) { std::println("[{}] declined: {}", r.index(), d.reason); },
[&](const NetworkError& n) { std::println("[{}] retry in {} s", r.index(), n.retryAfter); },
}, r);
if (auto* s = std::get_if<Success>(&all[0])) std::println("first txn: {}", s->txn);
std::println("last holds Declined? {}", std::holds_alternative<Declined>(all[2]));
}আউটপুট পাঁচ লাইন: [0] paid 500 taka, id TX7781, [1] declined: low balance, [2] retry in 30 s, first txn: TX7781 আর last holds Declined? false. বন্ধনীর সংখ্যাগুলো index() — তালিকায় টাইপের ক্রম। vector-এ তিনটি ভিন্ন struct সরাসরি রাখা গেছে কোনো inheritance, pointer বা heap ছাড়াই, কারণ প্রত্যেকটি একটি PayResult. শেষ ফলটি NetworkError, তাই Declined আছে কি না প্রশ্নের উত্তর false. lambda-গুলো [&] দিয়ে r-কে ধরেছে, শুধু index ছাপানোর জন্য।
JSON-এর মতো একটি মান
variant-এর আরেকটি পরিচিত ব্যবহার এমন মান, যা কয়েকটি মৌলিক টাইপের যেকোনোটি হতে পারে — যেমন একটি JSON ফাইলের একটি ঘর:
using Json = std::variant<std::nullptr_t, bool, int, double, std::string>;
Json a = 42, b = 3.5, c = std::string("Dhaka"), d = true, e = nullptr;
// সত্যিকারের JSON-এ তালিকা আর object-ও থাকে, যার ভেতরে আবার Json —
// তখন নিজের ভেতরে নিজেকে রাখতে vector বা unique_ptr-এর মতো একটি ধাপ লাগে।একটি ফাঁদের কথা জেনে রাখুন। std::variant<bool, std::string> v = "hi"; — এখানে "hi" একটি const char*, যা bool-এও রূপান্তর হতে পারে, string-এও। C++17-এর মূল নিয়মে এটি bool বেছে নিত, আর v হয়ে যেত true! C++20-এ একটি সংশোধনী (P0608) এই রূপান্তরের নিয়ম কড়া করেছে, তাই আধুনিক GCC-তে এখন string বাছাই হয় (index 1). পুরোনো compiler-এ কোড চালাতে হলে স্পষ্ট লিখুন: std::string("hi").
৬ · নতুন টাইপ যোগ করলে compile error — exhaustiveness-এর উপহার
এখন সেই কথা, যার জন্য variant-কে অনেকে এত পছন্দ করেন। ছয় মাস পরে কেউ Shape-এ একটি Triangle যোগ করলেন। পুরোনো ধাঁচের switch হলে কী হতো, আপনি জানেন: যে switch-এ Triangle-এর শাখা নেই, সেটি চুপচাপ default-এ যেত, বা কিছুই করত না। visit-এ কী হয়, দেখুন:
using Shape = std::variant<Circle, Rect>;
double area(const Shape& s) {
return std::visit(overloaded{
[](const Circle& c) { ... },
[](const Rect& r) { ... },
}, s);
}
struct Triangle { double b, h; };
using Shape = std::variant<Circle, Rect, Triangle>;
// area()-এ কেউ হাত দেয়নি।
// perimeter(), draw(), toJson()-এও না।
In instantiation of 'std::visit(...)
[with _Visitor = overloaded<...Circle&..., ...Rect&...>;
_Variants = const variant<Circle, Rect, Triangle>&]':
prog.cc:8:22: required from here
error: no type named 'type' in
'struct std::invoke_result<overloaded<...>,
const Triangle&>'
return std::visit(overloaded{
[](const Circle& c) { ... },
[](const Rect& r) { ... },
[](const Triangle& t) { return 0.5 * t.b * t.h; },
}, s);
// সতর্কতা: [](const auto&) { return 0.0; } দিয়ে
// “বাকি সব” ধরলে এই নিরাপত্তা হারিয়ে যায়।
- সব ঠিক আছে। Shape-এ দুটি টাইপ, visitor-এ দুটি lambda. compiler প্রতিটি টাইপের জন্য একটি মানানসই lambda পেয়েছে, তাই কোড compile হয়।
- শুধু টাইপ-তালিকা বদলাল। একজন Triangle যোগ করলেন — একটি নতুন struct আর
usingলাইনে একটি শব্দ। বড় প্রজেক্টে area-র মতো visit হয়তো দশ জায়গায় আছে, বিভিন্ন ফাইলে, বিভিন্ন মানুষের লেখা। কেউ মনে রাখবে না সব কোথায়। - compiler প্রতিটি জায়গা ধরিয়ে দেয়। visit এখন Triangle-এর জন্যও একটি ঘর বানাতে চায়, কিন্তু visitor-কে
const Triangle&দিয়ে ডাকা যায় না। ফল compile error — বার্তাটি লম্বা আর template-এ ভরা (এখানে ছোট করে দেখানো), কিন্তু আসল তথ্যটি শেষে আছে:const Triangle&. যে কয়টি visit Triangle সামলায় না, প্রতিটিতে একটি করে error. একটিও নীরবে বাদ পড়ে না। - সমাধান এক লাইন। Triangle-এর lambda যোগ করলে আবার compile হয়। কিন্তু একটি ফাঁদ আছে: শেষে একটি
autolambda রেখে “বাকি সব” ধরলে নতুন টাইপও চুপচাপ সেখানে চলে যাবে — switch-এরdefault-এর মতোই। তাই সাধারণ নিয়ম: যেখানে প্রতিটি টাইপের আলাদা অর্থ আছে, সেখানে catch-all রাখবেন না।
এই গুণটির নাম exhaustiveness — প্রতিটি সম্ভাবনা সামলানো হয়েছে কি না, compiler-এর যাচাই। Rust-এ এটি ভাষারই নিয়ম — কোনো বিকল্প বাদ পড়লে compile error; Haskell বা OCaml-এ compiler অন্তত সতর্কবার্তা দেয় (ভাবনার প্রশ্ন ৪)। C++-এ এটি আসে library-র কৌশল থেকে, তাই error message সুন্দর নয় — কিন্তু নিরাপত্তাটি পুরোপুরি আসল। উল্টো দিক থেকে ভাবুন: compile error এখানে বন্ধু। যে ভুল compile-এর সময় ধরা পড়ে, তা কোনো গ্রাহকের ফোনে পৌঁছায় না।
যখন variant “কিছুই না” হয়ে যায় — valueless_by_exception
variant-এর একটি বিরল কোণ আছে। v-তে একটি int আছে, আর আপনি লিখলেন v = BigThing{...};. variant-কে আগে int ধ্বংস করতে হয়, তারপর জায়গায় BigThing তৈরি করতে হয়। কিন্তু যদি BigThing-এর constructor মাঝপথে exception ছোড়ে? পুরোনো int ইতিমধ্যে নেই, নতুন BigThing-ও তৈরি হয়নি। তখন variant ঢোকে একটি বিশেষ অবস্থায়: valueless by exception. এ অবস্থায় v.valueless_by_exception() দেয় true, v.index() দেয় std::variant_npos, আর visit বা get ছোড়ে bad_variant_access. আবার একটি মান বসালেই variant স্বাভাবিক হয়।
বাস্তবে এটি প্রায় দেখাই যায় না — কেবল তখনই সম্ভব, যখন কোনো টাইপের তৈরি বা কপি exception ছুড়তে পারে, আর সেই exception আপনি ধরে variant-টি আবার ব্যবহার করছেন। সাধারণ int, double, string-এর মতো টাইপে move করার সময় exception হয় না, আর library অনেক ক্ষেত্রে আগে একটি অস্থায়ী কপি বানিয়ে ঝুঁকি এড়িয়ে চলে। কেন standard এই অবস্থাটি রাখল, সেটি একটি চমৎকার নকশা-বিতর্ক — ভাবনার প্রশ্ন ৩-এ।
৭ · variant বনাম virtual — বন্ধ তালিকা বনাম খোলা তালিকা
পর্ব ২৫-এ একই সমস্যা — নানা ধরনের Payment বা Shape — সমাধান করেছিলেন inheritance আর virtual function দিয়ে। এখন আরেকটি সমাধান হাতে এল। কোনটি কখন? উত্তরটি সুন্দরভাবে দুটি প্রশ্নে ভাগ করা যায়: নতুন টাইপ বেশি যোগ হবে, নাকি নতুন কাজ (operation) বেশি যোগ হবে?
virtual (পর্ব ২৫) — খোলা টাইপ-তালিকা
নতুন একটি Shape যোগ করা সহজ: একটি নতুন class, তার নিজের area(), draw(). পুরোনো কোড বদলায় না, এমনকি অন্য কেউ plugin হিসেবে যোগ করতে পারে। কিন্তু নতুন একটি কাজ (ধরুন toJson()) যোগ করা কঠিন: base-এ নতুন virtual ফাংশন, আর প্রতিটি derived class খুলে সেটি লেখা।
variant — বন্ধ টাইপ-তালিকা
নতুন একটি কাজ যোগ করা সহজ: একটি নতুন ফাংশন, ভেতরে একটি visit — কোনো struct খুলতে হয় না। কিন্তু নতুন একটি টাইপ যোগ করলে প্রতিটি visit খুলে নতুন lambda লিখতে হয় (compiler অবশ্য সবগুলো দেখিয়ে দেয়)। আর বাইরের কেউ তালিকায় টাইপ যোগ করতে পারে না।
নতুন কাজ সস্তা · নতুন টাইপ দামিএই টানাপোড়েনের একটি বিখ্যাত নাম আছে: expression problem. Philip Wadler ১৯৯৮ সালে নামটি দেন, যদিও সমস্যাটি তার আগেই পরিচিত ছিল। প্রশ্নটা হলো — এমন নকশা কি সম্ভব, যেখানে নতুন টাইপ আর নতুন কাজ দুটোই পুরোনো কোড না বদলে যোগ করা যায়, আর টাইপ-নিরাপত্তাও থাকে? সাধারণ object-oriented নকশা এক দিকে সহজ, sum type অন্য দিকে। তাই প্রশ্নটা “কোনটি ভালো” নয়, বরং “আপনার সমস্যায় কোন দিকটা বেশি বদলায়”।
| প্রশ্ন | std::variant + visit | inheritance + virtual |
|---|---|---|
| টাইপের তালিকা | বন্ধ, compile-এর সময় জানা | খোলা, যে কেউ যোগ করতে পারে |
| মেমরি | মান হিসেবে, heap ছাড়া; মাপ = সবচেয়ে বড় টাইপ + tag | সাধারণত heap-এ, pointer দিয়ে; object-প্রতি vptr |
| সাধারণ base দরকার? | না — যেকোনো টাইপ, এমনকি int বা string | হ্যাঁ, সবাইকে একই base থেকে আসতে হয় |
| সব ক্ষেত্র সামলানো হয়েছে? | compiler যাচাই করে | base-এর virtual ফাংশন থাকলেই চলে |
| মানানসই উদাহরণ | পেমেন্টের ফল, parser-এর token, state machine-এর অবস্থা | GUI widget, plugin, ডিভাইস ড্রাইভার |
variant-এর একটি বাড়তি সুবিধা value semantics: std::vector<Shape> লিখলে সব আকৃতি পাশাপাশি মেমরিতে থাকে, কপি করলে সত্যিকারের কপি হয়, কোনো unique_ptr লাগে না। দামটিও জেনে রাখুন: একটি ছোট টাইপ আর একটি বিশাল টাইপ একই variant-এ থাকলে প্রতিটি variant বিশালটির মাপ নেয়। তেমন হলে বড় টাইপটিকে std::unique_ptr-এ রাখুন, বা নকশা ভাবুন। দুটি পথই dynamic dispatch — চলার সময় টাইপ দেখে কোড বাছাই — শুধু টেবিলটি থাকে ভিন্ন জায়গায়।
৮ · std::any — “যেকোনো কিছু”, আর কেন প্রায়ই ভুল উত্তর
optional বলে “হয়তো একটি T”, variant বলে “এই তালিকার একটি”। std::any বলে “যেকোনো টাইপের একটি মান — কোনটি, তা আমি টাইপে বলব না”। এর ভেতরে মানটি ছাড়াও থাকে টাইপের তথ্য, আর পড়তে হয় std::any_cast দিয়ে, ঠিক কোন টাইপ আশা করছেন তা বলে দিয়ে:
#include <any>
std::any a = 42; // ভেতরে int
std::any_cast<int>(a); // 42
std::any_cast<long>(a); // throw std::bad_any_cast — int ≠ long!
if (auto* p = std::any_cast<int>(&a)) { /* pointer দিলে: ভুল টাইপে nullptr */ }
a = std::string("Dhaka"); // এখন string — any নিজেও টাইপ বদলাতে পারে
a = "Dhaka"; // সাবধান: এটি const char*, string নয়
a.has_value(); a.reset(); a.type(); // type() একটি typeid তথ্য দেয়লক্ষ করুন any_cast<long>: ভেতরে int থাকলে long চাইলেও exception — যদিও int সহজেই long-এ রূপান্তর হয়। any_cast টাইপ হুবহু মেলায়, কোনো রূপান্তর করে না। ভুল টাইপে ছোড়ে std::bad_any_cast. আর a = "Dhaka" রাখে একটি const char* — পরে any_cast<std::string> চাইলে আবার exception. এই দুটি ভুলই compile-এর সময় ধরা পড়ে না, কারণ any-র টাইপ থেকে compiler কিছুই জানে না।
any হলো type erasure-এর একটি উদাহরণ — আসল টাইপটি বাইরে থেকে মুছে ফেলা। এর দাম তিনটি। এক, টাইপ-যাচাই চলার সময়, তাই ভুল ধরা পড়ে চালানোর পর, compile-এর সময় নয়। দুই, বড় মান সাধারণত heap-এ রাখা হয় (ছোট মান library ভেতরেই রাখতে পারে — small buffer optimization), যা variant কখনো করে না। তিন, কোডটি নিজেই বলে না কোন কোন টাইপ আসতে পারে — পাঠককে পুরো প্রোগ্রাম খুঁজে বের করতে হয়। আর ভেতরে রাখা টাইপকে কপি করা যেতে হয় (copy-constructible), তাই move-only টাইপ সরাসরি রাখা যায় না।
যখন টাইপের তালিকা সত্যিই আপনার জানা নেই, আর জানার কথাও নয়। যেমন একটি event বা GUI লাইব্রেরি, যা ব্যবহারকারীকে প্রতিটি বোতামের সাথে “নিজের কিছু তথ্য” জুড়ে রাখতে দেয় — লাইব্রেরি তথ্যটি শুধু বয়ে নিয়ে বেড়ায়, কখনো ভেতরে দেখে না। C-তে এই কাজ হতো void* দিয়ে; any তার নিরাপদ রূপ, কারণ ভুল টাইপে পড়লে অন্তত exception আসে। কিন্তু যে মুহূর্তে আপনি লিখতে পারেন “এটি int, double বা string হবে” — variant নিন। নিয়মটা সোজা: তালিকা জানা থাকলে variant, তালিকা অজানা হলে any — আর প্রায় সবসময় তালিকা জানা থাকে।
এক অনুচ্ছেদে tuple আর structured binding
এই পর্বের টাইপগুলোকে পর্ব ১২-এর পরিচিত টাইপগুলোর পাশে রাখলে ছবিটি সম্পূর্ণ হয়। struct, std::pair আর std::tuple বলে “এটা এবং ওটা” — একসাথে সবগুলো মান থাকে (তত্ত্বের ভাষায় product type). variant বলে “এটা অথবা ওটা” (sum type), আর optional হলো “T অথবা কিছু না”। structured binding দিয়ে product type সহজে খোলা যায়: auto [ok, marks] = ...;. একসময় “হয়তো একটি মান” বোঝাতে অনেকে std::pair<bool, int> ফেরত দিতেন — কিন্তু তাতে ok-এর মান false হলেও marks-এ কিছু একটা থাকে, আর কেউ ভুল করে সেটি পড়তে পারে। optional সেই ফাঁক বন্ধ করেছে: মান না থাকলে পড়ার মতো কিছুই নেই। সংক্ষেপে: সবগুলো একসাথে হলে struct বা tuple, একটি মাত্র হলে variant, থাকতেও পারে না-ও পারে হলে optional.
তিনটি টাইপেরই জন্ম Boost লাইব্রেরিতে, standard-এ আসার অনেক আগে। Kevlin Henney-র লেখা Boost.Any এসেছিল ২০০১ সালের দিকে, Fernando Cacciola-র Boost.Optional আর Eric Friedman ও Itay Maman-এর Boost.Variant তার কয়েক বছরের মধ্যে। দশ বছরেরও বেশি সময় ধরে হাজারো প্রজেক্টে ব্যবহৃত হয়ে, পরীক্ষিত হয়ে, তারপর এরা C++17-এ std:: নাম পায় — অনেক ক্ষেত্রে নকশায় কিছু পরিবর্তনসহ। C++23 optional-এ monadic ফাংশন যোগ করেছে, আর C++26-এ variant-এর ভেতরেই একটি visit member function আর optional<T&> এসেছে।
- optional-এ
value()আর*-এর পার্থক্য কী? — খালি optional-এvalue()std::bad_optional_access ছোড়ে;*কিছু যাচাই করে না, তাই খালি হলে undefined behavior. - C-এর union আর std::variant-এর পার্থক্য? — union কোন সদস্য সক্রিয় তা মনে রাখে না, constructor/destructor ডাকে না; variant একটি tag রাখে, ঠিক destructor চালায়, ভুল টাইপ পড়লে exception দেয়।
std::getআরstd::get_if-এর পার্থক্য? — ভুল টাইপে get ছোড়ে std::bad_variant_access; get_if নেয় variant-এর pointer আর দেয় nullptr.- variant কখন valueless হয়? — টাইপ বদলানোর সময় নতুন মান তৈরিতে exception হলে, যখন পুরোনো মান ইতিমধ্যে ধ্বংস; তখন index() দেয় variant_npos.
- variant নাকি virtual? — টাইপের তালিকা বন্ধ ও জানা হলে আর নতুন কাজ বেশি যোগ হলে variant; বাইরের কেউ নতুন টাইপ যোগ করবে এমন হলে virtual.
৯ · AI-এর যুগে — “নেই” আর “এগুলোর একটি” ঠিকমতো লেখা হয়েছে কি না
AI টুলগুলো শিখেছে বিপুল পরিমাণ পুরোনো C আর C++ কোড থেকে, যেখানে optional বা variant ছিলই না। তাই না চাইলে তারা প্রায়ই সেই পুরোনো রীতিতে ফেরে: খুঁজে না পেলে -1, string না পেলে "", object না পেলে nullptr. আবার optional ব্যবহার করলেও প্রায়ই যাচাই ছাড়া *opt লেখে, কারণ ছোট উদাহরণে সেটি সংক্ষিপ্ত দেখায়। আর “নানা টাইপের ডেটা” শুনলেই কখনো কখনো std::any টেনে আনে, যেখানে variant অনেক ভালো হতো। সাধারণ ভুলগুলো:
| AI যা করে | সমস্যা | যা হওয়া উচিত |
|---|---|---|
না পেলে -1, "" বা nullptr ফেরত | “নেই” টাইপে লেখা নেই; যাচাই ভুললে নীরব ভুল | std::optional<T>; কারণ দরকার হলে std::expected |
যাচাই ছাড়া *opt বা opt-> | খালি হলে undefined behavior | if (opt) তারপর *opt, অথবা value_or |
পরপর std::get<T>(v), কোনো যাচাই ছাড়া | ভুল টাইপে exception; কোন টাইপ সামলানো হলো বোঝা যায় না | std::visit + overloaded; একটি টাইপ হলে get_if |
index() দেখে switch, সাথে default | নতুন টাইপ এলে নীরবে default-এ যায় | visit — compiler নতুন টাইপ ধরিয়ে দেয় |
“নানা টাইপের মান” মানেই std::any | টাইপ-যাচাই চলার সময়; any_cast-এ সহজে ভুল | তালিকা জানা হলে std::variant |
visitor-এ [](auto&) catch-all | নতুন টাইপের জন্য compile error হারিয়ে যায় | প্রতিটি টাইপের জন্য নিজের lambda |
AI-কে কোড লিখতে বলার সময় নিয়মগুলো শুরুতেই দিয়ে দিন, আর প্রতিটি সিদ্ধান্তের কারণ জানতে চান:
AI-এর কোড যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১Tony Hoare null reference-কে কেন তাঁর “বিলিয়ন ডলারের ভুল” বললেন? আর বিভিন্ন ভাষা এই ভুলের উত্তর কীভাবে দিয়েছে?
Tony Hoare — quicksort-এর আবিষ্কারক, আর পরে Turing Award পাওয়া কম্পিউটার বিজ্ঞানী — ১৯৬৫ সালে ALGOL W ভাষার নকশা করছিলেন। তাঁর লক্ষ্য ছিল, প্রতিটি reference-এর ব্যবহার যেন সম্পূর্ণ নিরাপদ হয়, compiler নিজে তা যাচাই করে। কিন্তু একটি “null reference” রাখার লোভ তিনি সামলাতে পারেননি, কারণ বাস্তবায়ন করা খুব সহজ ছিল। ২০০৯ সালে লন্ডনের QCon সম্মেলনে “Null References: The Billion Dollar Mistake” নামের বক্তৃতায় তিনি এই সিদ্ধান্তের জন্য প্রকাশ্যে দুঃখ প্রকাশ করেন। তাঁর যুক্তি ছিল: এই একটি সিদ্ধান্ত থেকে অসংখ্য ভুল, দুর্বলতা আর crash জন্মেছে, যার মোট ক্ষতি সম্ভবত শত কোটি ডলারেরও বেশি। সংখ্যাটি একটি আন্দাজ, হিসাব নয় — কিন্তু বার্তাটি স্পষ্ট।
আসল সমস্যা null নিজে নয়, বরং null-কে প্রতিটি reference টাইপের ভেতরে ঢুকিয়ে দেওয়া। Java-তে একটি String টাইপের ভ্যারিয়েবল আসলে বলে “একটি String, অথবা null” — আর টাইপ-ব্যবস্থা দুটিকে আলাদা করে না। ফলে প্রতিটি ব্যবহারের আগে যাচাই করা উচিত, কিন্তু কোনটি সত্যিই null হতে পারে তা কোড দেখে বোঝা যায় না। হয় আপনি সব জায়গায় অকারণ যাচাই করেন, নয়তো কোথাও ভুলে যান।
আধুনিক ভাষাগুলো সমাধান খুঁজেছে একই দিকে: “থাকতে পারে না-ও পারে” কথাটিকে টাইপে আলাদা করে লেখা। ML আর Haskell-এ বহু আগে থেকেই option আর Maybe ছিল। Rust-এ সাধারণ reference কখনো null নয়; না থাকার সম্ভাবনা থাকলে লিখতে হয় Option<T>, আর মান বের করার আগে compiler সেটি সামলাতে বাধ্য করে। Swift আর Kotlin-এ String আর String? দুটি আলাদা টাইপ। Java ৮ (২০১৪) Optional যোগ করেছে, যদিও পুরোনো reference-গুলো এখনো null হতে পারে। C++17-এর std::optional একই ধারার অংশ। তবে C++ পুরোনো কোডের সাথে সামঞ্জস্য রাখে, তাই pointer আর nullptr রয়ে গেছে — optional একটি সুযোগ, বাধ্যবাধকতা নয়। আর C++-এর reference (T&) নিজেই কখনো null হওয়ার কথা নয়, যা ভাষার একটি পুরোনো সুবুদ্ধি।
মূল উপলব্ধি: ভুলটা “null” ধারণায় নয়, ভুলটা হলো “না থাকা”-কে টাইপে অদৃশ্য রাখা। যে সম্ভাবনা টাইপে লেখা থাকে, compiler সেটি মনে করিয়ে দিতে পারে; যা লেখা থাকে না, তা মানুষকে মনে রাখতে হয় — আর মানুষ ভোলে।
প্র ০২optional, variant আর any প্রথমে Boost-এ এসেছিল, standard-এ এল C++17-এ। এই দীর্ঘ পথের কারণ কী, আর পথে নকশা কীভাবে বদলাল?
Boost হলো C++-এর একটি বিশাল মুক্ত লাইব্রেরি-সংগ্রহ, যার শুরু ১৯৯৮ সালের দিকে, আর যার প্রতিষ্ঠাতাদের কয়েকজন ছিলেন C++ standard কমিটিরই সদস্য। শুরু থেকেই এর একটি অঘোষিত উদ্দেশ্য ছিল standard-এর জন্য পরীক্ষাগার হওয়া: নতুন লাইব্রেরি আগে Boost-এ আসবে, হাজারো প্রজেক্টে ব্যবহৃত হবে, তার ভুল আর দুর্বলতা বেরিয়ে আসবে, তারপর সেই অভিজ্ঞতা নিয়ে standard-এর প্রস্তাব লেখা হবে। smart pointer, regex, thread, filesystem — অনেক কিছু এই পথে এসেছে।
Boost.Any (Kevlin Henney), Boost.Optional (Fernando Cacciola) আর Boost.Variant (Eric Friedman ও Itay Maman) ২০০০-এর দশকের শুরুতে এসেছিল। optional-কে standard-এ আনার প্রস্তাব C++14-এর সময় বেশ এগিয়েছিল — খসড়ায় ঢুকেও শেষ পর্যায়ে বিতর্কের কারণে সরিয়ে নেওয়া হয়, আর একটি আলাদা পরীক্ষামূলক দলিলে (Library Fundamentals TS) রাখা হয়। শেষে তিনটিই C++17-এ জায়গা পায়।
পথে নকশার বেশ কিছু প্রশ্ন নতুন করে ভাবতে হয়েছে। প্রথমত reference: Boost.Optional optional<T&> সমর্থন করত, কিন্তু assignment-এ rebind হবে নাকি assign-through — এই বিতর্ক মীমাংসা না হওয়ায় standard এটি বাদ দেয়; মীমাংসা হতে লাগল প্রায় এক দশক, C++26 পর্যন্ত। দ্বিতীয়ত variant-এর “কখনো খালি নয়” নিশ্চয়তা: Boost.Variant দাবি করত variant সবসময় কোনো না কোনো মান রাখবে, আর এর জন্য প্রয়োজনে heap-এ একটি অস্থায়ী কপি রাখত। standard-এর কমিটি দীর্ঘ বিতর্কের পরে heap বাদ দিয়ে “valueless by exception” অবস্থা মেনে নেয় (প্রশ্ন ৩)। তৃতীয়ত Boost.Variant-এ visit করার একটি আলাদা visitor-ক্লাসের রীতি ছিল; C++17 আসার আগে lambda আর variadic template চলে আসায় standard-এর std::visit অনেক সরল হতে পেরেছে।
এই দীর্ঘ পথের কারণ C++-এর একটি মূল বাস্তবতা: standard-এ একবার কিছু ঢুকলে তা প্রায় কখনো বদলানো যায় না, কারণ কোটি কোটি লাইনের কোড তার ওপর নির্ভর করবে। তাই কমিটি সতর্ক, ধীর, আর বাস্তব অভিজ্ঞতার ওপর নির্ভরশীল।
মূল উপলব্ধি: ভালো standard আসে ব্যবহারের অভিজ্ঞতা থেকে, শুধু কাগজের নকশা থেকে নয়। Boost-এ কাটানো বছরগুলো এই টাইপগুলোর ভুল খুঁজে বের করেছে, যাতে standard সংস্করণটি টেকসই হয়।
প্র ০৩std::variant কেন “valueless” হতে পারে? কমিটির সামনে আর কী কী বিকল্প ছিল, আর প্রতিটির দাম কী?
সমস্যার মূলে একটি সরল ক্রম। variant-এর ভেতরে একটি জায়গা, আর তাতে এখন একটি A আছে। আপনি সেখানে একটি B বসাতে চান। একই জায়গায় দুটি object একসাথে থাকতে পারে না, তাই আগে A ধ্বংস করতে হয়, তারপর B তৈরি করতে হয়। B-এর constructor যদি মাঝপথে exception ছোড়ে, তখন A আর নেই, B-ও নেই। variant তাহলে কী রাখবে?
কমিটির সামনে কয়েকটি পথ ছিল। এক — দুটি জায়গা (double buffering): variant-এর মাপ দ্বিগুণ করা, যাতে আগে নতুন জায়গায় B তৈরি করে সফল হলে তবেই A ধ্বংস করা যায়। নিরাপদ, কিন্তু প্রতিটি variant-এর মেমরি দ্বিগুণ — “যা ব্যবহার করেন না, তার দাম দেবেন না” নীতির বিরুদ্ধে। দুই — heap-এ ব্যাকআপ: Boost.Variant-এর পথ; দরকার হলে A-কে heap-এ সরিয়ে রেখে চেষ্টা করা, ব্যর্থ হলে ফেরত আনা। এতে variant কখনো খালি হয় না, কিন্তু একটি সাধারণ assignment হঠাৎ heap থেকে মেমরি চাইতে পারে, যা অনেক সিস্টেমে অগ্রহণযোগ্য, আর আচরণ জটিল। তিন — শুধু nothrow টাইপ: variant-এ কেবল এমন টাইপ রাখা যাবে, যাদের তৈরি কখনো exception ছোড়ে না। নিরাপদ, কিন্তু অনেক বাস্তব টাইপ বাদ পড়ে। চার — খালি অবস্থা মেনে নেওয়া: standard এটিই বেছে নেয়, আর অবস্থাটির নাম দেয় “valueless by exception”, যাতে নামেই বোঝা যায় এটি কেবল exception-এর ফলে ঘটে।
দাম কী? প্রতিটি visit আর get-কে এই অবস্থা যাচাই করতে হয়, আর ব্যবহারকারীর কাছে variant আর “সবসময় একটি মান” নয়। কিন্তু বাস্তবে এটি খুব বিরল, কারণ যে exception এটি ঘটায় সেটি আপনি ধরে ফেলে variant আবার ব্যবহার না করলে খালি অবস্থাটি কখনো দেখাই যায় না। library বাস্তবায়নও যখন পারে, আগে একটি অস্থায়ী object তৈরি করে, তারপর nothrow move দিয়ে বসায় — তাতে ঝুঁকি আরও কমে।
লক্ষ করুন, এটি সেই একই প্রশ্ন, যা পর্ব ২২-এ copy-and-swap আর strong exception guarantee-তে দেখেছিলেন: মাঝপথে ব্যর্থ হলে object কোন অবস্থায় থাকবে? প্রতিটি উত্তরই কোথাও না কোথাও দাম নেয়।
মূল উপলব্ধি: নিখুঁত নিশ্চয়তা কখনো বিনামূল্যে আসে না। C++ এখানে বেছে নিয়েছে “সাধারণ ক্ষেত্রে শূন্য খরচ, বিরল ক্ষেত্রে একটি স্পষ্ট, নাম দেওয়া অবস্থা” — দামটি লুকানো নয়, ঘোষণা করা।
প্র ০৪Haskell-এর data বা Rust-এর enum-কে বলা হয় algebraic data type. এদের তুলনায় std::variant কোথায় পিছিয়ে, আর কোথায় এগিয়ে?
algebraic data type-এর ধারণা এসেছে ML-পরিবারের আর Hope-এর মতো functional ভাষা থেকে, ১৯৭০ আর ১৯৮০-র দশকে। “algebraic” কারণ টাইপগুলো জোড়া লাগে যোগ আর গুণের মতো: struct বা tuple হলো গুণ (সব মান একসাথে, সম্ভাব্য অবস্থার সংখ্যা গুণ হয়), আর “এটা অথবা ওটা” হলো যোগ (অবস্থার সংখ্যা যোগ হয়)। Haskell-এ লেখা হয় data Shape = Circle Double | Rect Double Double; Rust-এ enum Shape { Circle(f64), Rect(f64, f64) }. দুটিই আসলে আমাদের std::variant<Circle, Rect>.
পার্থক্যটা হলো, ওসব ভাষায় এটি ভাষার মূল অংশ, আর C++-এ একটি library. এর কয়েকটি ফল আছে। প্রথমত নাম: Rust-এ প্রতিটি বিকল্পের নিজের নাম আছে, তাই একই f64 দুই বিকল্পে থাকলেও কোনো সমস্যা নেই। std::variant-এ বিকল্পকে চেনা হয় টাইপ দিয়ে, তাই variant<int, int> অস্বস্তিকর, আর প্রতিটি বিকল্পের জন্য ছোট struct বানাতে হয়। দ্বিতীয়ত pattern matching: Rust-এর match একসাথে বিকল্প চেনে, ভেতরের মান খুলে নামে বাঁধে, আর কোনো বিকল্প বাদ পড়লে স্পষ্ট ভাষায় error দেয় — “এই pattern-টি সামলানো হয়নি”। C++-এর visit-এ একই নিরাপত্তা আছে, কিন্তু template-এর লম্বা error message-সহ। তৃতীয়ত মেমরি: Rust-এর compiler জানে একটি reference কখনো null নয়, তাই Option<&T> ঠিক একটি pointer-এর মাপ — “খালি” বোঝাতে সে null মানটিকেই ব্যবহার করে। std::optional<T*>-এ এমন কৌশল standard-এ নেই, আলাদা চিহ্ন লাগে।
তবে variant-এর কিছু নিজস্ব শক্তিও আছে। যেকোনো টাইপ সরাসরি বিকল্প হতে পারে — int, std::string, বা অন্য কারও লাইব্রেরির class — আগে থেকে কোনো ঘোষণা ছাড়া। একই টাইপ অনেক variant-এ ব্যবহার করা যায়। আর template-এর সাথে মিলিয়ে generic কোড লেখা যায়, যেমন একটি generic lambda দিয়ে সব বিকল্পকে একসাথে ছাপানো।
মূল উপলব্ধি: ধারণাটি একই — sum type আর exhaustiveness — শুধু বাস্তবায়নের স্তর আলাদা। ভাষায় থাকলে ব্যবহার সহজ ও পরিষ্কার; library-তে থাকলে নমনীয়, কিন্তু কিছুটা কষ্টসাধ্য। C++ এই ফাঁক বন্ধ করতে pattern matching-এর দিকে তাকিয়ে আছে (প্রশ্ন ৫)।
প্র ০৫std::visit-এর ব্যবহার নিয়ে এত সমালোচনা কেন? আর C++-এ pattern matching আনার প্রস্তাবগুলো কী বলছে?
সমালোচনার তালিকা বেশ লম্বা। প্রথমত, overloaded-এর মতো একটি মৌলিক টুকরো standard library-তে নেই; প্রত্যেককে নিজে লিখতে হয়, আর নতুন কেউ সেই এক লাইনের using Ts::operator()... দেখে ঘাবড়ে যান। দ্বিতীয়ত, ভুল হলে error message বিশাল — এই পর্বেই দেখেছেন, একটি অনুপস্থিত lambda-র জন্য GCC-র বার্তা template-এর নামে ভরা কয়েক ডজন লাইন। তৃতীয়ত, সব lambda-কে একই টাইপ ফেরত দিতে হয়। চতুর্থত, ভেতরের মান খোলা (destructure) যায় না, “এই Rect, যদি w ০ হয়” এমন শর্ত লেখা যায় না, আর visit-এর ভেতর থেকে বাইরের লুপে break বা ফাংশন থেকে সরাসরি return করা যায় না, কারণ প্রতিটি শাখা আলাদা একটি lambda. ২০১৭ সালে Matt Kline-এর একটি বহুল আলোচিত ব্লগ পোস্টের শিরোনামই ছিল — “std::visit is everything wrong with modern C++”. শিরোনামটি অতিরঞ্জিত, কিন্তু অভিযোগটি অনেকের মনের কথা: নিরাপদ জিনিসটি ব্যবহার করা অকারণে কঠিন।
সমাধান হিসেবে বহু বছর ধরে ভাষায় pattern matching আনার প্রস্তাব চলছে। P1371 প্রস্তাবে (Michael Park, David Sankel, Bjarne Stroustrup প্রমুখ) inspect নামের একটি নতুন গঠন ছিল। Herb Sutter-এর P2392 প্রস্তাব দিয়েছিল is আর as শব্দ দিয়ে একটি সাধারণ কাঠামো। পরে Michael Park-এর P2688 প্রস্তাব match expression নিয়ে এসেছে, যেখানে variant-এর প্রতিটি বিকল্পের জন্য একটি শাখা, ভেতরের মান খোলা, শর্ত দেওয়া আর exhaustiveness যাচাই — সব এক জায়গায়। এগুলো এখনো প্রস্তাব; C++26-এ pattern matching গৃহীত হয়নি, আর ভবিষ্যতের কোন সংস্করণে, কোন রূপে আসবে তা নিশ্চিত নয়।
এর মাঝে ছোট ছোট উন্নতি এসেছে। C++20 থেকে overloaded-এর জন্য deduction guide লিখতে হয় না। C++26 variant-এ একটি member visit যোগ করেছে, যাতে v.visit(f) লেখা যায়। আর C++23-এর deducing this visitor লেখার কিছু কৌশল সহজ করেছে।
মূল উপলব্ধি: একটি ভাষায় নিরাপদ পথটি যদি অস্বস্তিকর হয়, মানুষ অনিরাপদ পথে ফেরে — switch আর get দিয়ে। তাই ergonomics শুধু সৌন্দর্যের প্রশ্ন নয়, নিরাপত্তারও প্রশ্ন। ততদিন overloaded-এর ছোট কৌশলটিই C++-এর কাজের উত্তর।
অনুশীলন
বেশিরভাগ কাজে কোড লিখতে হবে না — আগে অনুমান করুন, তারপর উত্তর মিলিয়ে নিন।
-
আউটপুট অনুমান:
std::optional<int> a;std::variant<int, std::string> v;— এরপর ক্রমে:a.value_or(5),a = 3; a.has_value(),v.index(),v = std::string("x"); v.index(), আরstd::get_if<int>(&v) == nullptr. পাঁচটি ফল কী?৫ (a খালি, তাই বিকল্প মান); true (এখন 3 আছে); 0 (ডিফল্ট variant প্রথম টাইপ int ধরে, মান 0); 1 (এখন string, তালিকার দ্বিতীয় টাইপ); true (ভেতরে int নেই, তাই get_if nullptr দেয়)। খেয়াল করুন, তৃতীয় ধাপে variant “খালি” নয় — ডিফল্টভাবে সবসময় প্রথম টাইপের একটি মান থাকে।
-
বাগ শিকার: একটি AI এই কোড দিয়েছে —
int priceOf(const std::string& item), যা না পেলে -1 দেয়; তারপরint total = priceOf("dal") + priceOf("chal");. আরেক জায়গায়std::optional<int> stock = getStock(id); std::println("{}", *stock);. কয়টি সমস্যা পান, আর কীভাবে ঠিক করবেন?দুটি মূল সমস্যা। (১) priceOf-এর -1 একটি sentinel: কোনো একটি জিনিস না পেলে মোট দাম এক টাকা কম আসবে, আর দুটিই না পেলে -2 — কেউ টের পাবে না। ঠিক করুন
std::optional<int> priceOf(...)দিয়ে; তারপর মোট হিসাবের আগে দুটিই যাচাই করুন, বা কোন জিনিস পাওয়া যায়নি তা জানাতে চাইলেstd::expected(পর্ব ৩৭)। (২)*stockযাচাই ছাড়া: getStock খালি দিলে undefined behavior. লিখুনif (stock) ..., বাstock.value_or(0)যদি শূন্য ধরা অর্থপূর্ণ হয়, বাstock.value()যদি খালি হওয়া সত্যিই একটি ভুল। -
নকশা ভাবুন: প্রতিটি ক্ষেত্রে কোনটি নেবেন — optional, variant, any, expected, নাকি virtual? (ক) ব্যবহারকারীর মাঝের নাম, যা অনেকের নেই (খ) একটি ক্যালকুলেটরের token: সংখ্যা, অপারেটর বা বন্ধনী (গ) একটি ছবি-আঁকার অ্যাপ, যেখানে বাইরের ডেভেলপাররা plugin দিয়ে নতুন আকৃতি যোগ করবেন (ঘ) একটি ফাইল খোলার ফাংশন, যার ব্যর্থতার কারণ ব্যবহারকারীকে দেখাতে হবে (ঙ) একটি GUI লাইব্রেরি, যা প্রতিটি বোতামের সাথে ব্যবহারকারীর নিজের যেকোনো তথ্য রাখতে দেয়।
(ক)
std::optional<std::string>— না থাকা স্বাভাবিক, কারণ জানার দরকার নেই। (খ)std::variant<Number, Op, Paren>— তালিকা ছোট, বন্ধ, আর parser-এর প্রতিটি ধাপে সব ধরন সামলাতে হবে। (গ) inheritance আর virtual — টাইপের তালিকা খোলা, অন্যরা যোগ করবেন, আপনার কোড আবার compile না করেই। (ঘ)std::expected<File, Error>— ব্যর্থতার কারণ গুরুত্বপূর্ণ। (ঙ)std::any— লাইব্রেরি তথ্যের টাইপ জানে না, শুধু বয়ে নেয়; এটি any-র সেই বিরল সঠিক জায়গা। -
নিজে লিখুন (কোড): উপরের
pay_result.cpp-এ একটি নতুন টাইপ যোগ করুন:struct Refunded { int taka; };, আরPayResult-এর তালিকায় সেটি বসান। কোনো lambda যোগ না করে compile করুন — কী দেখেন? তারপর lambda যোগ করে vector-এ একটিRefunded{200}রাখুন। নতুন আউটপুট লাইনটি কী হবে?শুধু তালিকায় যোগ করলে compile error আসবে — বার্তার শেষে
const Refunded&দিয়ে visitor ডাকা যাচ্ছে না, এমন কথা থাকবে। এটাই exhaustiveness: compiler অসম্পূর্ণ visit ধরিয়ে দিল। তারপর যোগ করুন[&](const Refunded& f) { std::println("[{}] refunded {} taka", r.index(), f.taka); }আর vector-এর শেষেRefunded{200}. নতুন লাইন:[3] refunded 200 taka— index 3, কারণ Refunded তালিকার চতুর্থ টাইপ। শেষ দুটি লাইনও বদলাবে না, কারণall[2]এখনো NetworkError.
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমC++-এর std::visit(visitor, v) ডাকের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- compile-এর সময় যাচাই হয়, visitor-কে variant-এর প্রতিটি টাইপ দিয়ে ডাকা যায় কি না
- প্রতিটি টাইপের জন্য একটি ঘরসহ dispatch-এর টেবিল তৈরি হয়
- চলার সময় variant-এর tag (index) পড়া হয়
- tag অনুযায়ী টেবিলের ঘর থেকে ঠিক সংস্করণটি বাছাই হয়
- ভেতরের মানের reference দিয়ে সেই সংস্করণ ডাকা হয়, আর তার ফল visit ফেরত দেয়
C++-এ একটি খালি std::optional<int> o;-এর ওপর o.value() ডাকলে কী হয়?
- 0 ফেরত দেয়
- Undefined behavior
- std::bad_optional_access exception ছোড়ে
- Compile error
C++-এ std::variant<int, std::string> v = 5; লেখার পরে std::get<std::string>(v) ডাকলে কী হয়?
- "5" লেখাটি ফেরত দেয়
- std::bad_variant_access exception ছোড়ে
- nullptr ফেরত দেয়
- ভেতরের বাইটগুলো string হিসেবে পড়ে আবর্জনা দেয়
একটি C++ ফাংশন রোল নম্বর দিয়ে ছাত্র খোঁজে; না পাওয়া স্বাভাবিক, আর কেন পাওয়া গেল না তা জানার দরকার নেই। ফেরত-টাইপ হিসেবে কোনটি সবচেয়ে উপযুক্ত?
- std::optional<Student>
- Student*, না পেলে nullptr
- std::any
- Student, না পেলে নাম ছাড়া একটি ফাঁকা Student
C++-এ using Shape = std::variant<Circle, Rect>;-এ নতুন একটি Triangle যোগ করা হলো, কিন্তু একটি std::visit(overloaded{...}, s)-এ শুধু Circle আর Rect-এর lambda আছে, catch-all নেই। কী ঘটে?
- Triangle-এর জন্য visit কিছুই করে না
- চলার সময় Triangle এলে exception
- Triangle Rect-এর lambda-তে চলে যায়
- Compile error
C++-এ std::any a = 42; লেখার পরে std::any_cast<long>(a) ডাকলে কী হয়?
- 42 ফেরত দেয়, long হিসেবে
- std::bad_any_cast exception ছোড়ে
- Compile error
- nullptr ফেরত দেয়
C++-এ std::variant<int, std::string> v; — কোনো মান না দিয়ে তৈরি করলে v-এর অবস্থা কী?
- valueless — কোনো মান নেই
- খালি, std::nullopt-এর মতো
- ভেতরে int 0, index() 0
- ভেতরে খালি string, index() 1
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- যা সত্যি, তা টাইপে লিখুন: -1,
""বা nullptr দিয়ে “নেই” বোঝানো একটি গোপন চুক্তি; optional আর variant সেই চুক্তিকে compiler-এর চোখের সামনে আনে। - optional পড়ার তিন উপায়:
value()খালি হলে exception,*optখালি হলে UB,value_or(x)খালি হলে x. C++23-এand_then/transform/or_else; কারণ দরকার হলেstd::expected. - variant = একটি জায়গা + একটি tag. ভুল টাইপে
getexception দেয়,get_ifnullptr; ডিফল্টভাবে প্রথম টাইপ; “খালি” চাইলেstd::monostate. - visit + overloaded: প্রতিটি টাইপের নিজের lambda; নতুন টাইপ যোগ করলে অসম্পূর্ণ visit compile হয় না — catch-all দিয়ে এই নিরাপত্তা নষ্ট করবেন না।
- বন্ধ তালিকা হলে variant, খোলা তালিকা হলে virtual, আর any কেবল তখন, যখন টাইপের তালিকা সত্যিই অজানা।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৩৯ · constexpr — compile-এর সময়েই হিসাবপরবর্তী পর্বকোন হিসাব প্রোগ্রাম চলার আগেই করা যায়, আর তাতে কী লাভ।
- C++ প্রোগ্রামিং · কোর্স হোম৫৫টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- পর্ব ৩৭ · exception আর std::expectedআবার দেখুন“নেই”-এর সাথে কারণও জানাতে হলে — expected আর exception-এর পুরো আলোচনা।
- C প্রোগ্রামিং · struct ও unionC কোর্সহাতে বানানো tagged union — variant-এর ভেতরে যা লুকানো আছে, তা খোলাখুলি।