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

exception আর std::expected — ভুল সামলানো

Exceptions and std::expected — Designing How Failures Travel

Taught in Bangla, this lesson covers C++ error handling: throw, try and catch by const reference, the std::exception hierarchy and custom exceptions, stack unwinding with RAII, the nothrow, strong and basic guarantees, noexcept and std::terminate, and C++23 std::expected with and_then.

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

  • ব্যর্থতাকে তিন ভাগে ভাগ করা, আর প্রতিটির জন্য সঠিক যন্ত্র বেছে নেওয়া
  • throw, try, catch, standard exception-এর পরিবার, আর নিজের exception class বানানো
  • Stack unwinding আর RAII কীভাবে exception-কে নিরাপদ করে; throw; আর catch (...)-এর সঠিক জায়গা
  • Exception safety guarantee, noexcept, std::terminate, আর exception-এর আসল খরচ
  • C++23-এর std::expected, তার monadic শৃঙ্খল, একটি সিদ্ধান্তের টেবিল, আর AI-এর লেখা কোডে ভুল-সামলানোর ভুল ধরা

১ · ভুল নকশারই অংশ — কে সামলাবে, আর সে কীভাবে জানবে?

প্রতিটি আসল প্রোগ্রামে কিছু না কিছু ভুল হবেই। ব্যবহারকারী টাকার ঘরে লিখবেন “১২x”, সার্ভারের config ফাইল কেউ মুছে ফেলবেন, নেটওয়ার্ক মাঝপথে কেটে যাবে, ডিস্ক ভরে যাবে। নতুন প্রোগ্রামাররা প্রায়ই ভাবেন, “আগে আসল কাজটা লিখি, ভুল সামলানো পরে যোগ করব।” কিন্তু অভিজ্ঞ প্রকৌশলীরা জানেন, পরে যোগ করা error handling প্রায় সবসময় জোড়াতালি হয়। কারণ ভুল সামলানো আসলে একটি নকশার সিদ্ধান্ত, শুধু কয়েকটি if নয়।

একটি garments কারখানার কথা ভাবুন। সেলাই মেশিনের সুই ভাঙলে অপারেটর নিজেই সুই বদলে নেন — খবর ওপরে যাওয়ার দরকার নেই। কিন্তু একটি পুরো কাপড়ের লট যদি ভুল রঙের আসে, অপারেটর কিছু করতে পারেন না। তিনি লাইন সুপারভাইজারকে জানান, সুপারভাইজার প্রোডাকশন ম্যানেজারকে, আর ম্যানেজার সিদ্ধান্ত নেন — সরবরাহকারীকে ফেরত দেবেন, নাকি অন্য অর্ডারে লাগাবেন। আর আগুন লাগলে কেউ কাউকে জিজ্ঞেস করে না, ঘণ্টা বাজে আর সবাই বেরিয়ে যান। একই কারখানায় তিন রকম সমস্যা, তিন রকম খবর পাঠানোর পথ। সমস্যা সামলায় সেই মানুষ, যার কাছে সিদ্ধান্ত নেওয়ার মতো তথ্য আর ক্ষমতা আছে।

প্রোগ্রামেও ঠিক এমন। যে ফাংশন ভুলটা প্রথম টের পায়, সে প্রায়ই জানে না এখন কী করা উচিত। একটি readFile ফাংশন জানে ফাইলটি নেই, কিন্তু জানে না এটা সার্ভারের নিজের config (তাহলে প্রোগ্রাম চালানোই অর্থহীন), নাকি ব্যবহারকারীর বেছে নেওয়া একটি ছবি (তাহলে “ফাইল পাওয়া যায়নি, আবার বেছে নিন” বললেই চলে)। তাই প্রতিটি ব্যর্থতার জন্য দুটি প্রশ্ন:

মূল প্রশ্ন দুটি

১. কে এই ব্যর্থতা সামলাতে পারে? ঠিক ডাকার ফাংশন (caller), নাকি অনেক স্তর ওপরের কেউ, নাকি কেউই না? ২. সে খবরটা কীভাবে পাবে? রিটার্ন মানের ভেতরে, যাতে উপেক্ষা করা কঠিন হয়? নাকি একটি exception হিসেবে, যা মাঝের সব স্তর পেরিয়ে সোজা তার কাছে পৌঁছায়? এই পর্বের বাকি সব কিছু এই দুটি প্রশ্নের উত্তর।

উত্তর খুঁজতে আগে ব্যর্থতাগুলোকে তিন ভাগে ভাগ করুন:

বাগ — প্রোগ্রামারের ভুল

নিজের লুপের index সীমা পেরোল, যে pointer কখনো null হওয়ার কথা নয় সেটি null, ফাংশনের নথিতে লেখা শর্ত (precondition) ভাঙা হলো। এগুলো “সামলানো” যায় না — কোড ঠিক করতে হয়। যন্ত্র: assert, আর C++26 থেকে contracts. যত তাড়াতাড়ি সম্ভব, জোরে থামুন।

fix the code

প্রত্যাশিত ব্যর্থতা

ব্যবহারকারী ভুল সংখ্যা লিখলেন, PIN ভুল হলো, খোঁজা নামটি তালিকায় নেই। এগুলো স্বাভাবিক কাজেরই অংশ, প্রায়ই ঘটে, আর সাধারণত ঠিক ডাকার ফাংশনই সামলায়। যন্ত্র: রিটার্ন মান — std::expected, বা কারণ লাগলে না, শুধু “নেই” বোঝাতে std::optional.

return it

ব্যতিক্রমী ব্যর্থতা

সার্ভার চালু হওয়ার সময় config ফাইল নেই, মেমরি শেষ (std::bad_alloc), constructor তার রিসোর্স নিতে পারল না। কালেভদ্রে ঘটে, আর সামলাতে পারে অনেক স্তর ওপরের কেউ। যন্ত্র: exception.

throw it

খেয়াল করুন, ভাগটা পরিস্থিতির, ভুলের নামের নয়। “ফাইল নেই” একটি ছবি-সম্পাদক অ্যাপে প্রত্যাশিত (ব্যবহারকারী ভুল ফাইল বেছেছেন), আর একটি ব্যাংকের সার্ভারের নিজের config-এর ক্ষেত্রে ব্যতিক্রমী। একই ফাংশনের দুই রকম ব্যবহারকারী থাকলে কখনো দুটি রূপই দেওয়া হয় — C++-এর std::filesystem ঠিক এটাই করে, পরে দেখবেন।

C ভাষায় এই সব কিছুর একটাই উপায় ছিল: রিটার্ন কোড। ফাংশন -1 বা NULL ফেরত দেয়, আর কারণ রাখে global errno-তে। এর দুর্বলতা হলো: উপেক্ষা করা খুব সহজ, ভালো ফল আর ভুলের সংকেত একই চ্যানেলে মেশে (-1 কি বৈধ তাপমাত্রা হতে পারে না?), আর খবর দশ স্তর ওপরে পাঠাতে মাঝের প্রতিটি ফাংশনে হাতে যাচাই লিখতে হয়। C++ এর দুটি উত্তর দিয়েছে: exception (খবর নিজে থেকেই ওপরে ওঠে) আর std::expected (ব্যর্থতা রিটার্ন মানের টাইপেই লেখা থাকে)। দুটিই এই পর্বে।

২ · throw, try, catch — খারাপ খবর ওপরে পাঠানো

Exception-এর ব্যবস্থায় তিনটি অংশ। যে ফাংশন ভুল টের পায়, সে একটি throw দিয়ে একটি exception object “ছোড়ে” — ভুলের বিবরণসহ একটি সাধারণ C++ object. যে অংশ সামলাতে পারে, সে কোডটিকে একটি try block-এ রাখে, আর তার পরে একটি catch লিখে বলে দেয়, কোন টাইপের exception সে ধরবে। মাঝের ফাংশনগুলো? তাদের কিছুই লিখতে হয় না।

throw চলার সঙ্গে সঙ্গে স্বাভাবিক পথ থেমে যায়। প্রোগ্রাম call stack বেয়ে ওপরে উঠতে থাকে — আগে বর্তমান ফাংশনে, তারপর তাকে যে ডেকেছে, তারপর তাকে যে ডেকেছে — যতক্ষণ না মিলে যাওয়ার মতো একটি catch পাওয়া যায়। একটি সার্ভার চালুর তিন স্তরের উদাহরণ দেখুন: startServer ডাকে loadConfig-কে, সে ডাকে readFile-কে, আর ফাইলটি নেই। প্রতিটি ফাংশনে একটি ছোট Guard object আছে, যা ঢোকার আর বের হওয়ার সময় প্রিন্ট করে — যাতে আপনি দেখতে পান কে কখন বিদায় নিচ্ছে:

#include <print>
#include <stdexcept>
#include <string>
struct ConfigError : std::runtime_error {      // nijer exception type
    using std::runtime_error::runtime_error;
};
struct Guard {                                 // RAII: destructor dekhay unwinding
    const char* name;
    explicit Guard(const char* n) : name(n) { std::println("enter {}", name); }
    ~Guard() { std::println("leave {}", name); }
};
void readFile(const std::string& path) {
    Guard g("readFile");
    throw ConfigError("missing file: " + path);
}
void loadConfig()  { Guard g("loadConfig");  readFile("app.conf"); }
void startServer() { Guard g("startServer"); loadConfig(); std::println("server up"); }
int main() {
    try { startServer(); }
    catch (const ConfigError& e) { std::println("caught: {}", e.what()); }
}

আউটপুট আসে এই ক্রমে: enter startServer, enter loadConfig, enter readFile, তারপর leave readFile, leave loadConfig, leave startServer, আর সবশেষে caught: missing file: app.conf. তিনটি জিনিস লক্ষ করুন। এক, server up কখনো প্রিন্ট হয়নি — throw-এর পরের কোনো স্বাভাবিক কোড চলে না। দুই, loadConfig আর startServer-এ ভুল সামলানোর একটি লাইনও নেই, তবু খবরটা তাদের পেরিয়ে main-এ পৌঁছেছে। তিন, catch চলার আগেই তিনটি leave এসেছে, ভেতর থেকে বাইরের দিকে। এটাই পর্ব ১৭-এ দেখা stack unwinding, এবার তিনটি frame জুড়ে।

ভেতরে ঠিক কী ঘটে, ধাপে ধাপে দেখুন। প্রতিটি ধাপে আগে নিজে অনুমান করুন, তারপর “পরে” চাপুন:

1void readFile(const std::string& path) {2    Guard g("readFile");3    throw ConfigError("missing file: " + path);4}5void loadConfig()  { Guard g("loadConfig");  readFile(...); }6void startServer() { Guard g("startServer"); loadConfig(); ... }7try { startServer(); }8catch (const ConfigError& e) { std::println("caught: {}", e.what()); }
call stack-এ কোন frame আছে
readFile
loadConfig
startServer
exception
  1. প্রথম frame. main-এর try-এর ভেতর থেকে startServer ডাকা হলো। তার নিজের frame তৈরি হলো, আর তাতে g জন্মাল।
  2. দ্বিতীয় frame. startServer ডাকল loadConfig-কে। এখন stack-এ দুটি frame, প্রতিটিতে একটি জীবিত Guard.
  3. তৃতীয় frame. loadConfig ডাকল readFile-কে। এখানেই আসল কাজ — আর এখানেই প্রথম জানা যায় যে ফাইলটি নেই।
  4. Throw. readFile জানে ফাইল নেই, কিন্তু জানে না কী করা উচিত। তাই সে একটি ConfigError object বানিয়ে ছুড়ে দেয়। object-টি কোনো frame-এর ভেতরে থাকে না — runtime তাকে একটি আলাদা জায়গায় রাখে, যাতে frame ভাঙলেও সে বেঁচে থাকে। শুরু হয় catch খোঁজা।
  5. readFile গুটানো। readFile-এ কোনো try নেই। তাই তার frame ছেড়ে যেতে হবে — আর ছাড়ার আগে তার local g-এর destructor চলে: leave readFile.
  6. loadConfig গুটানো। loadConfig-এও catch নেই। একই কাজ: তার g ধ্বংস, frame বাদ। মাঝের ফাংশন কিছু না লিখেও নিজের রিসোর্স ঠিকঠাক ফেরত দিয়েছে।
  7. startServer গুটানো। একইভাবে তৃতীয় frame-ও গুটিয়ে যায়। server up লাইনটি কখনো চলে না, কারণ loadConfig() স্বাভাবিকভাবে ফেরেনি।
  8. Catch মিলল। main-এর catch (const ConfigError&) টাইপে মিলে যায়। e এখন সেই একই object-এর দিকে reference, আর e.what() দেয় throw-এর সময় লেখা বার্তা। catch শেষ হলে exception object নিজেও ধ্বংস হয়, আর প্রোগ্রাম catch-এর পরের লাইন থেকে চলে।

কেন সবসময় const& দিয়ে ধরবেন

catch-এর প্যারামিটার ফাংশনের প্যারামিটারের মতোই। তিনভাবে লেখা যায় — by value, by reference, by const reference. নিয়ম একটাই: by const reference দিয়ে ধরুন।

catch (std::exception e)         { /* ভুল: কপি হয়, আর slicing */ }
catch (const std::exception& e)  { /* ঠিক: আসল object-টিই দেখা যায় */ }

by value ধরলে exception object কপি হয় — অকারণ খরচ। আর বেশি বিপজ্জনক: যদি ছোড়া হয় একটি ConfigError, আর আপনি ধরেন std::exception by value, তাহলে শুধু ভিত্তি-অংশটুকু কপি হয় — বাকি অংশ কেটে বাদ পড়ে। পর্ব ২৪-এ দেখা এই সমস্যার নাম object slicing. তখন e.what() আর আসল বার্তা দেয় না, দেয় শুধু ভিত্তি class-এর সাধারণ লেখা। reference দিয়ে ধরলে কোনো কপি নেই, আর virtual ফাংশন what() আসল টাইপেরটাই চলে। const বলে দেয় যে catch ব্লক exception-টি বদলাবে না।

একাধিক catch থাকলে C++ ওপর থেকে নিচে প্রথম যেটি মেলে, সেটিই বেছে নেয় — সবচেয়ে ভালো মিলটি নয়। তাই নির্দিষ্ট টাইপ আগে, সাধারণ টাইপ পরে লিখুন। catch (const std::exception&) আগে লিখলে নিচের catch (const ConfigError&) কখনো চলবে না; g++ এ নিয়ে একটি warning-ও দেয়।

৩ · std::exception-এর পরিবার — আর নিজের exception class

C++-এ আসলে যেকোনো টাইপ ছোড়া যায় — throw 42; বা throw "error"; পর্যন্ত। কিন্তু তা করবেন না। standard library-র সব exception একটি পরিবারের সদস্য, যার শীর্ষে std::exception (<exception> header). এর একটি virtual ফাংশন আছে, what(), যা ভুলের একটি বর্ণনা দেয়। পরিবারের সবাই এর derived class, তাই একটি মাত্র catch (const std::exception& e) সবাইকে ধরতে পারে। মূল সদস্যরা (<stdexcept> আর <new>-এ):

শাখাকী বোঝায়কে ছোড়ে (উদাহরণ)
std::logic_errorডাকার মানুষের যুক্তির ভুল — চলার আগেই এড়ানো যেতনিচের দুটির মা-শাখা
↳ std::invalid_argumentপ্যারামিটারের মানই অর্থহীনstd::stoi("abc")
↳ std::out_of_rangeসীমার বাইরের index বা মানv.at(10), যখন v-তে ৩টি উপাদান
std::runtime_errorএমন অবস্থা, যা শুধু চলার সময়েই জানা যায়নিজের ConfigError, std::system_error
std::bad_allocমেমরি পাওয়া গেল না (সরাসরি std::exception থেকে)new, আর vector বা string বড় হতে গিয়ে
std::bad_optional_access, std::bad_expected_accessখালি optional বা ভুল-ভরা expected থেকে মান চাওয়াopt.value(), exp.value()

logic_error আর runtime_error-এর পার্থক্যটা দর্শনের: logic_error বলে “আপনি আমাকে ভুলভাবে ডেকেছেন”, runtime_error বলে “আপনি ঠিকই ডেকেছেন, কিন্তু পৃথিবী সহযোগিতা করেনি।” বাস্তবে অনেক অভিজ্ঞ প্রোগ্রামার মনে করেন, logic_error-এর বেশিরভাগ ক্ষেত্র আসলে বাগ, আর বাগের জন্য exception নয় — assert বা contract ভালো (এ নিয়ে ৮ নম্বর অংশে)। তাই নিজের exception সাধারণত runtime_error থেকে বানানো হয়।

নিজের exception class

নিজের ভুলের জন্য একটি আলাদা টাইপ বানালে ডাকার মানুষ ঠিক সেই ভুলটিকেই আলাদা করে ধরতে পারেন। সবচেয়ে সরল রূপটি আগের কোডেই দেখেছেন: using std::runtime_error::runtime_error; লাইনটি ভিত্তি class-এর constructor ধার করে আনে — পর্ব ২৪-এ দেখা inheriting constructor. বাড়তি তথ্য লাগলে সদস্য যোগ করুন:

class ConfigError : public std::runtime_error {
public:
    ConfigError(const std::string& file, int line)
        : std::runtime_error(file + ":" + std::to_string(line) + ": bad value"),
          line_(line) {}
    int line() const noexcept { return line_; }   // ডাকার মানুষ লাইন নম্বর পায়
private:
    int line_;
};

throw ConfigError("app.conf", 12);   // what() দেয় "app.conf:12: bad value"

কয়েকটি নিয়ম। exception class-কে std::exception-এর পরিবারে রাখুন, যাতে main-এর একটি সাধারণ catch সবাইকে ধরতে পারে। এর কপি করা যেন ব্যর্থ না হয় — runtime_error নিজের বার্তা এমনভাবে রাখে যে কপিতে নতুন মেমরি লাগে না, তাই নিজে বড় std::string সদস্য না রেখে বার্তাটা ভিত্তি class-কে দিন। আর value হিসেবে ছুড়ুন: throw ConfigError(...), কখনো throw new ConfigError(...) নয়। Java বা C#-এর অভ্যাসে লেখা new একটি pointer ছোড়ে, যা const ConfigError& ধরতে পারে না, আর কেউ delete না করলে memory leak.

C জানলে

C-তে exception নেই; ভুল জানানো হয় রিটার্ন কোড আর errno দিয়ে, আর ওপরে পাঠাতে প্রতিটি স্তরে হাতে যাচাই লিখতে হয়। C++ রিটার্ন কোড বাদ দেয়নি — std::expected আসলে সেই রীতিরই টাইপ-নিরাপদ, উপেক্ষা-করা-কঠিন রূপ। নতুন হলো exception: খবর নিজে থেকে ওপরে ওঠে, আর পথে destructor পরিষ্কারের কাজ করে, যা C-তে goto cleanup দিয়ে করতে হয়। C কোর্সের এরর হ্যান্ডলিং পর্বে দেখুন রিটার্ন কোডের রীতি কতটা শৃঙ্খলা চায়।

৪ · Stack unwinding আর RAII — exception কেন নিরাপদ হয়

Exception-এর সবচেয়ে বড় ভয় ছিল: মাঝপথে ফাংশন ছেড়ে গেলে নেওয়া রিসোর্সগুলোর কী হবে? পর্ব ১৭-এ এর উত্তর দেখেছেন — stack unwinding-এর সময় প্রতিটি local object-এর destructor চলে। তাই রিসোর্স যদি RAII object-এ থাকে (std::vector, std::unique_ptr, std::ofstream, std::lock_guard), exception-এর পথেও সব ফেরত যায়। উল্টোটাও সত্য: হাতে new করা মেমরি বা হাতে খোলা ফাইল থাকলে exception সেটিকে leak করবে। exception আর RAII তাই আলাদা বিষয় নয় — একে অপরকে ছাড়া অসম্পূর্ণ। আধুনিক C++ কোড exception-safe হয় মূলত এই কারণে যে তাতে খোলা new/delete প্রায় নেই।

আবার ছোড়া — throw;

কখনো কোনো ফাংশন ভুলটা সামলাতে পারে না, কিন্তু কিছু একটা করে খবরটা ওপরে পাঠাতে চায় — যেমন একটি লগ লিখতে। তখন catch-এর ভেতরে শুধু throw; লিখুন, কোনো নাম ছাড়া। একে বলে rethrow:

try { postToLedger(txn); }
catch (const std::exception& e) {
    log("ledger failed: ", e.what());
    throw;        // ঠিক: একই object, একই আসল টাইপ ওপরে যায়
    // throw e;   // ভুল: নতুন কপি, আর টাইপ কেটে std::exception হয়ে যায়
}

throw e; লিখলে e-এর ঘোষিত টাইপ (std::exception) অনুযায়ী একটি নতুন কপি ছোড়া হয় — আবার সেই slicing. ওপরের catch (const ConfigError&) তখন আর মেলে না। throw; মূল object-টিকেই আবার পাঠায়। (catch-এর বাইরে, কোনো চলমান exception ছাড়া throw; লিখলে প্রোগ্রাম std::terminate হয়।)

catch (...) — শুধু সীমানায়

catch (...) যেকোনো কিছু ধরে, এমনকি std::exception-এর পরিবারের বাইরের টাইপও। তাই এর মধ্যে আপনি জানেন না কী ধরেছেন। এর বৈধ জায়গা মাত্র কয়েকটি — প্রোগ্রামের সীমানা, যেখান দিয়ে exception পার হতে দেওয়া যায় না বা উচিত নয়:

  • main-এর একেবারে বাইরের স্তর — শেষবারের মতো একটি বার্তা লিখে ঠিক exit code দিয়ে বেরোনোর জন্য।
  • একটি thread-এর প্রথম ফাংশন — thread থেকে exception বেরোলে পুরো প্রোগ্রাম terminate হয় (পর্ব ৪৫)।
  • C লাইব্রেরির ডাকা callback — C কোড exception চেনে না, তাই সেখানে exception-কে একটি রিটার্ন কোডে বদলাতে হয়।
  • destructor-এর ভেতরে, ব্যর্থ হতে পারে এমন পরিষ্কারের কাজের চারপাশে (নিচে দেখুন)।

এর বাইরে, বিশেষ করে catch (...) {} — খালি ব্লক — প্রায় সবসময় একটি বাগ। এটি ভুলের খবর গিলে ফেলে, আর প্রোগ্রাম আধা-বদলানো ডেটা নিয়ে এমনভাবে চলে যেন কিছুই হয়নি। সপ্তাহ পরে অন্য কোথাও অদ্ভুত ফল দেখা দেয়, আর আসল কারণ খুঁজে পাওয়া প্রায় অসম্ভব। সীমানাতেও catch (...)-এর কাজ হলো লগ করা আর রূপান্তর করা, চুপ করে থাকা নয়।

Destructor থেকে কখনো ছুড়বেন না

stack unwinding চলার সময় destructor-রা চলে। তখন যদি কোনো destructor নিজেও একটি exception ছোড়ে, একসাথে দুটি exception উড়বে — আর C++ সেটা সামলায় না, সরাসরি std::terminate ডাকে। এজন্য C++11 থেকে destructor ডিফল্টভাবে noexcept. কিন্তু পরিষ্কারের কাজও তো ব্যর্থ হতে পারে — যেমন ফাইল বন্ধ করার সময় বাকি লেখা ডিস্কে পাঠাতে গিয়ে ডিস্ক ভরা। রীতিটা হলো: ব্যর্থ-হতে-পারা কাজের জন্য একটি আলাদা ফাংশন দিন, আর destructor শুধু “শেষ চেষ্টা” করুক:

class LogFile {
public:
    void close();                // ব্যর্থ হলে throw — যে জানতে চায়, সে নিজে ডাকবে
    ~LogFile() {                 // noexcept (ডিফল্ট)
        try { if (open_) close(); }
        catch (...) { /* লগ করার চেষ্টা, তারপর চুপ — destructor-এর বাইরে নয় */ }
    }
private:
    bool open_ = false;
};

যে constructor ব্যর্থ হয়

constructor-এর কোনো রিটার্ন মান নেই, তাই রিসোর্স নিতে না পারলে ব্যর্থতা জানানোর সবচেয়ে পরিষ্কার উপায় হলো throw. পর্ব ২১-এ দেখেছেন, এর ফলাফল সুনির্দিষ্ট: object-টি কখনো “তৈরি” হয়নি ধরা হয়, তাই তার নিজের destructor চলে না। কিন্তু ততক্ষণে যে সদস্য আর ভিত্তি-অংশ পুরোপুরি তৈরি হয়ে গেছে, তাদের destructor চলে, উল্টো ক্রমে। এর শিক্ষা: সদস্যগুলোকে RAII টাইপ রাখুন। FILE* আর int* সদস্য রেখে constructor-এর মাঝপথে throw হলে সেগুলো leak করবে, কারণ যে destructor-এ আপনি fclose লিখেছিলেন, সেটি চলবেই না। আর constructor সফল মানেই object ব্যবহারযোগ্য — আলাদা init() ডাকার দরকার নেই।

৫ · Exception safety-র তিন প্রতিশ্রুতি — nothrow, strong, basic

RAII নিশ্চিত করে রিসোর্স leak হবে না। কিন্তু আরেকটি প্রশ্ন থেকে যায়: ব্যর্থতার পর object-গুলোর অবস্থা কী? একটি ফাংশন যদি দুটি হিসাব বদলাতে গিয়ে প্রথমটি বদলে দ্বিতীয়টিতে ব্যর্থ হয়, তাহলে ডেটা একটি অর্ধেক-বদলানো অবস্থায় পড়ে থাকে। এ প্রশ্নের উত্তর দিতে C++ সম্প্রদায় (Dave Abrahams-এর কাজের সূত্রে) তিনটি স্তরের exception safety প্রতিশ্রুতি ব্যবহার করে। প্রতিটি ফাংশন নিজের নথিতে বলে দেয়, সে কোন স্তরের প্রতিশ্রুতি দিচ্ছে:

প্রতিশ্রুতিমানেব্যাংকের ভাষায়উদাহরণ
nothrowকখনো exception ছোড়ে না, সবসময় সফলব্যালেন্স দেখাdestructor, swap, ভালো move constructor
strongসফল হয়, নয়তো সব কিছু ঠিক আগের মতো (commit-or-rollback)টাকা পাঠানো: হয় পুরো লেনদেন, নয়তো কিছুই নাvector::push_back (সাধারণ ক্ষেত্রে)
basicব্যর্থ হলেও কোনো leak নেই আর সব object বৈধ (invariant অটুট), তবে মান বদলে যেতে পারেদিনশেষের রিপোর্ট অর্ধেক তৈরি, কিন্তু খাতার হিসাব মেলেবেশিরভাগ সাধারণ ফাংশন

নিয়মটা এমন: প্রতিটি ফাংশন অন্তত basic দেবে, যেখানে সম্ভব আর অর্থবহ সেখানে strong, আর কয়েকটি মূল ফাংশন (destructor, swap, move) nothrow — কারণ বাকি দুটি প্রতিশ্রুতি এই nothrow ইটগুলোর ওপরেই গাঁথা হয়।

এবার সবচেয়ে চেনা উদাহরণ: রহিম করিমকে ২,০০০ টাকা পাঠাবেন। দুটি ধাপ — রহিমের হিসাব থেকে তোলা, করিমের হিসাবে জমা। ধরুন করিমের wallet-এর একটি সীমা আছে (উদাহরণে ২৫,০০০ টাকা), আর জমা দিতে গেলে সীমা পেরিয়ে যাবে, তাই দ্বিতীয় ধাপ exception ছোড়ে। strong guarantee দিতে হলে প্রথম ধাপটি উল্টে দিতে হবে:

1void transfer(Account& from, Account& to, int amt) {2    from.withdraw(amt);          // ধাপ ১3    try {4        to.deposit(amt);         // ধাপ ২ — ব্যর্থ হতে পারে5    } catch (...) {6        from.refund(amt);        // noexcept — ফিরিয়ে নেওয়া7        throw;                   // একই exception ওপরে8    }9}  // ডাকার জায়গায় catch বার্তা দেখায়
হিসাবের অবস্থা (টাকা)
রহিম
করিম
মোট
  1. শুরুর অবস্থা। রহিমের ৫,০০০, করিমের ২৪,০০০ — মোট ২৯,০০০. ব্যাংকের invariant: লেনদেনে মোট টাকা বদলায় না।
  2. ধাপ ১ সফল। রহিমের হিসাব থেকে ২,০০০ তোলা হলো। এই মুহূর্তে মোট ২৭,০০০ — invariant সাময়িকভাবে ভাঙা। এটা স্বাভাবিক, যদি ফাংশন শেষ হওয়ার আগে আবার ঠিক হয়।
  3. ধাপ ২ ব্যর্থ। করিমের সীমা পেরোবে, তাই deposit exception ছুড়ল। কিছুই না করলে exception সোজা বেরিয়ে যেত, আর ২,০০০ টাকা চিরতরে হাওয়া — এটা basic guarantee-ও নয়, কারণ invariant ভাঙা থেকে গেল।
  4. Rollback. catch (...) ধরল, আর refund টাকাটা রহিমকে ফেরত দিল। গুরুত্বপূর্ণ: refund নিজে nothrow হতে হবে। ফিরিয়ে নেওয়ার কাজটাই যদি ব্যর্থ হতে পারত, এই কৌশল কাজ করত না।
  5. throw; — এখানে catch (...) খবর গিলে ফেলছে না, শুধু পরিষ্কার করে একই exception আবার ওপরে পাঠাচ্ছে। আমরা জানি না কী ভুল হয়েছে, জানার দরকারও নেই — শুধু জানি যে আমাদের অর্ধেক কাজ উল্টাতে হবে।
  6. ফলাফল — strong guarantee. ডাকার জায়গার catch ব্যবহারকারীকে বার্তা দেখায়। হিসাব ঠিক আগের মতো: ৫,০০০ আর ২৪,০০০. বাইরে থেকে মনে হয় লেনদেনটা কখনো শুরুই হয়নি — হয় পুরোটা, নয়তো কিছুই না।

হাতে লেখা rollback ছোট ক্ষেত্রে চলে, কিন্তু ধাপ বাড়লে জটিল হয়ে যায়। সাধারণ, পরিষ্কার কৌশলটি পর্ব ২২-এ দেখেছেন: copy-and-swap. সব কাজ একটি কপির ওপর করুন — যা ব্যর্থ হলে আসলটির গায়ে আঁচড়ও লাগে না — তারপর একটি nothrow swap দিয়ে এক ঝটকায় “commit” করুন:

void Ledger::apply(const Batch& batch) {
    Ledger tmp = *this;              // কপি — ব্যর্থ হলে *this অক্ষত
    for (const auto& t : batch)
        tmp.post(t);                 // যেকোনো ধাপে throw হতে পারে
    std::swap(*this, tmp);           // nothrow (move noexcept হলে) — commit
}

দাম আছে: পুরো খাতার একটি কপি। বড় ডেটায় তাই strong guarantee সবসময় দেওয়া হয় না — standard library-ও সব জায়গায় দেয় না। এটি নকশার একটি সচেতন সিদ্ধান্ত।

move কেন noexcept হওয়া উচিত

std::vector::push_back সাধারণত strong guarantee দেয়। জায়গা ফুরালে সে নতুন বড় মেমরি নিয়ে পুরোনো উপাদানগুলো সেখানে সরায়। এখন মাঝপথে একটি move ব্যর্থ হলে পুরোনো উপাদানগুলোর কয়েকটি তো ইতিমধ্যে সরে গেছে — আগের অবস্থায় ফেরা যায় না। তাই vector উপাদান move করে শুধু তখন, যখন move constructor noexcept; নইলে নিরাপদে কপি করে, যা অনেক ধীর। পর্ব ৩৬-এ দেখা move constructor-এ noexcept লিখতে ভুলে গেলে এভাবে চুপচাপ গতি হারান।

৬ · noexcept, std::terminate আর exception-এর আসল খরচ

ফাংশনের পরে noexcept লিখলে আপনি একটি প্রতিশ্রুতি দেন: “এই ফাংশন থেকে কোনো exception বেরোবে না।” এর দুটি লাভ। পাঠক আর compiler দুজনেই জানেন, এই ফাংশন ডাকার পর ব্যর্থতার পথ ভাবতে হবে না — compiler কখনো আরও ছোট কোড বানাতে পারে। আর library (যেমন vector) এই তথ্য দেখে দ্রুততর পথ বেছে নেয়, যা উপরে দেখলেন। noexcept(expr) নামে একটি অপারেটরও আছে, যা compile-এর সময় বলে দেয় একটি এক্সপ্রেশন noexcept কি না — template কোডে এটি কাজে লাগে।

কিন্তু প্রতিশ্রুতি ভাঙলে কী হয়? compiler সাধারণত compile-এর সময় এটা আটকায় না (সবচেয়ে স্পষ্ট ক্ষেত্রে g++ একটি warning দেয়)। চলার সময় exception যদি একটি noexcept ফাংশনের সীমানায় পৌঁছায়, C++ আর catch খোঁজে না — সোজা std::terminate ডাকে, যা ডিফল্টভাবে প্রোগ্রাম বন্ধ করে দেয়। দেখুন:

1void save() noexcept {2    throw std::runtime_error("disk full");3}4int main() {5    try { save(); }6    catch (const std::exception& e) { std::println("caught"); }7}
কী ঘটছে
প্রতিশ্রুতি
main-এর catch
প্রোগ্রাম
  1. প্রতিশ্রুতিসহ ডাকা। main একটি try-এর ভেতর থেকে save() ডাকল। দেখে মনে হয়, ভুল হলে catch তো আছেই।
  2. Throw. save-এর ভেতরে exception ছোড়া হলো। এত স্পষ্ট ক্ষেত্রে g++ compile-এর সময়েই সতর্ক করে: এই throw সবসময় terminate ঘটাবে। কিন্তু সাধারণত throw থাকে আরও গভীরে, অন্য কোনো ফাংশনের ভেতরে — তখন কোনো warning আসে না।
  3. সীমানায় ধাক্কা। exception save-এর বাইরে বেরোতে চাইছে, কিন্তু save noexcept. C++ এখানেই থামে আর std::terminate ডাকে। ঠিক তার ওপরে একটি মিলে-যাওয়া catch থাকলেও কিছু যায় আসে না। stack আদৌ গুটানো হবে কি না, তা-ও implementation-এর ওপর নির্ভর করে — তাই destructor-রা চলবে, এমন ভরসা নেই।
  4. প্রোগ্রাম বন্ধ। g++-এর ডিফল্ট terminate handler exception-এর টাইপ আর what() প্রিন্ট করে abort করে। Linux-এর shell লেখে Aborted. শিক্ষা: noexcept তখনই লিখুন, যখন সত্যিই নিশ্চিত — এটি একটি অলংকার নয়, একটি চুক্তি।

কোথায় noexcept লিখবেন? destructor-এ এটা আগে থেকেই আছে। move constructor, move assignment আর swap-এ প্রায় সবসময় লিখুন — এগুলো সাধারণত শুধু pointer অদলবদল করে। কিন্তু “শুধু শুধু” সবখানে বসাবেন না: আজ যে ফাংশন ব্যর্থ হয় না, কাল তাতে একটি std::string তৈরি যোগ হলে সেটি bad_alloc ছুড়তে পারে, আর noexcept তখন প্রোগ্রাম বন্ধের ফাঁদ।

খরচের হিসাব — happy path বিনামূল্যে, throw দামি

আধুনিক compiler (Linux-এ GCC আর Clang, Windows-এর 64-bit MSVC) যে পদ্ধতি ব্যবহার করে, তাকে বলে zero-cost exception মডেল। নাম শুনে ভুল বুঝবেন না — এর মানে যতক্ষণ কিছু ছোড়া হয় না, ততক্ষণ প্রায় কোনো খরচ নেই। try-তে ঢুকতে কোনো বাড়তি নির্দেশ চলে না। তার বদলে compiler প্রোগ্রামের পাশে কিছু টেবিল রেখে দেয়: “এই ঠিকানার কোড থেকে exception বেরোলে কোন destructor চালাতে হবে, কোন catch দেখতে হবে।”

দাম দিতে হয় throw-এর মুহূর্তে। তখন runtime exception object-এর জন্য মেমরি নেয়, টেবিলগুলো খুঁজে প্রতিটি frame বুঝে নেয়, প্রতিটি catch-এর টাইপ RTTI দিয়ে মেলায়, আর destructor চালায়। একটি সাধারণ ফাংশন-রিটার্নের তুলনায় এটি বহু গুণ ধীর। এছাড়া টেবিলের জন্য প্রোগ্রামের আকার বাড়ে, আর throw-এর সম্ভাবনা কখনো কখনো optimizer-এর হাত বাঁধে। ফল একটি সরল নিয়ম: যে ঘটনা কালেভদ্রে ঘটে, তার জন্য exception দারুণ; যা প্রায়ই ঘটে (যেমন প্রতি সেকেন্ডে হাজারটি ভুল ইনপুট), তার জন্য রিটার্ন মান।

এই কারণগুলোয় কিছু ক্ষেত্রে পুরো প্রকল্পেই exception বন্ধ রাখা হয়, compiler-এ -fno-exceptions দিয়ে। অনেক গেম ইঞ্জিন এমন করে, কারণ প্রতিটি ফ্রেমের সময় বাঁধা আর অনিশ্চিত খরচ সেখানে অপছন্দের। ছোট embedded যন্ত্রে টেবিলের বাড়তি আকার আর heap-নির্ভরতা সমস্যা। Google-এর C++ style guide বহু বছর ধরে exception ব্যবহার না করার নিয়ম রেখেছে, আর তার একটি বড় কারণ হিসেবে বলেছে তাদের বিশাল পুরোনো কোড, যা exception-কে মাথায় রেখে লেখা হয়নি। এসব প্রকল্পে ভুলের খবর যায় রিটার্ন মানে — আর ঠিক এখানেই std::expected কাজে আসে।

৭ · std::expected — ব্যর্থতা রিটার্ন মানেরই অংশ

প্রত্যাশিত ব্যর্থতার জন্য exception ভারী, আর C-রীতির রিটার্ন কোড উপেক্ষা করা খুব সহজ। C++23 এর মাঝামাঝি একটি সমাধান দিয়েছে: std::expected<T, E> (<expected> header-এ আছে)। এটি একটি বাক্স, যার ভেতরে থাকে হয় একটি সফল মান T, নয়তো একটি ভুলের কারণ E — কখনো দুটিই নয়, কখনো কোনোটি বাদ নয়। ফাংশনের স্বাক্ষর দেখেই ডাকার মানুষ বোঝেন, এটি ব্যর্থ হতে পারে, আর ব্যর্থ হলে কী ধরনের কারণ পাওয়া যাবে।

bKash-এ টাকা পাঠানোর পর আপনি একটি বার্তা পান। সেটি হয় বলে “সফল, TrxID এত”, নয়তো বলে “ব্যর্থ — অপর্যাপ্ত ব্যালেন্স”। কখনো খালি বার্তা আসে না, আর ব্যর্থতার বার্তায় কারণ লেখা থাকে। std::expected ঠিক এমন একটি রসিদ: খুলে দেখতেই হবে কোনটি এসেছে।

একটি টাকার অঙ্ক লেখা থেকে পড়ার ফাংশন, সীমা যাচাই আর চার্জ যোগ — তিনটি ধাপের একটি ছোট প্রোগ্রাম দেখুন। এখানে সীমা ১০ থেকে ২৫,০০০ টাকা আর চার্জ ১% ধরা হয়েছে শুধু উদাহরণের জন্য:

#include <charconv>
#include <expected>
#include <print>
#include <string>
#include <string_view>
std::expected<int, std::string> parseAmount(std::string_view s) {   // taka, nahole karon
    int v = 0;
    auto [end, ec] = std::from_chars(s.data(), s.data() + s.size(), v);
    if (ec == std::errc{} && end == s.data() + s.size()) return v;
    return std::unexpected("not a number: " + std::string(s));
}
std::expected<int, std::string> checkLimit(int v) {
    if (v < 10 || v > 25000) return std::unexpected("out of limit: " + std::to_string(v));
    return v;
}
int main() {
    for (std::string_view in : {"500", "12x", "30000"}) {
        auto r = parseAmount(in).and_then(checkLimit).transform([](int v) { return v + v / 100; });
        if (r) std::println("{:>5} -> total {}", in, *r);
        else   std::println("{:>5} -> error: {}", in, r.error());
    }
}

আউটপুট ({:>5} ইনপুটটিকে ৫ ঘর চওড়া জায়গায় ডানে সাঁটে, তাই লাইনের শুরুতে কিছু ফাঁকা জায়গা থাকে): 500 -> total 505, 12x -> error: not a number: 12x, 30000 -> error: out of limit: 30000. সফল হলে ফাংশন সাধারণভাবে return v; লেখে — int নিজে থেকেই expected-এ বদলে যায়। ব্যর্থ হলে লেখে return std::unexpected(...), যা std::unexpected নামের একটি মোড়ক — “এটা মান নয়, ভুলের কারণ”। পড়ার কাজটি করেছে পর্ব ১৪-এর std::from_chars, যা নিজেও exception ছোড়ে না, একটি ফলাফল-কোড দেয়। আর main-এ if (r) দেখে মান আছে কি না, *r মানটি দেয়, r.error() কারণটি। কোথাও try/catch নেই, কোথাও ভুল চুপচাপ হারিয়ে যায়নি।

expected থেকে মান বের করার কয়েকটি উপায়, আর প্রতিটির দাম:

auto r = parseAmount(text);   // std::expected<int, std::string>
if (r.has_value()) use(*r);   // বা শুধু if (r) — আগে যাচাই, তারপর *r
int x = r.value_or(0);        // ভুল হলে একটি বিকল্প মান
r.error();                    // শুধু ভুল থাকলেই ডাকবেন, নইলে undefined behavior
r.value();                    // ভুল থাকলে std::bad_expected_access ছোড়ে

খেয়াল করুন, যাচাই না করে *r লিখলে আর ভেতরে ভুল থাকলে সেটা undefined behavior — compiler আপনাকে বাঁচাবে না। value() অন্তত একটি exception ছোড়ে। নিয়ম: আগে যাচাই, তারপর ব্যবহার — অথবা নিচের শৃঙ্খলের পদ্ধতি, যেখানে যাচাই নিজে থেকেই হয়।

শৃঙ্খল — and_then, transform, or_else

প্রোগ্রামের main-এ একটি লাইন লক্ষ করুন: parseAmount(in)-এর পরে .and_then(checkLimit), তারপর .transform(...). প্রতিটি ধাপের পর if না লিখে ধাপগুলো জোড়া হয়েছে। C++23-এ expected-এর এই monadic ফাংশনগুলো আছে:

  • and_then(f) — মান থাকলে f(মান) ডাকে; f নিজেও একটি expected ফেরত দেয়, অর্থাৎ নিজেও ব্যর্থ হতে পারে। ভুল থাকলে f ডাকেই না, ভুলটি এগিয়ে দেয়।
  • transform(f) — মান থাকলে তাকে বদলায়; f একটি সাধারণ মান ফেরত দেয়, যা ব্যর্থ হয় না (যেমন চার্জ যোগ)।
  • or_else(f) — উল্টো দিক: শুধু ভুল থাকলে f(ভুল) ডাকে, যা কখনো ভুল থেকে উদ্ধার করে একটি মান দিতে পারে।
  • transform_error(f) — ভুলের টাইপ বদলায়, যেমন নিচের স্তরের ভুলকে ওপরের স্তরের ভাষায়।

এর আসল শক্তি short-circuit: প্রথম ভুলের পর বাকি ধাপগুলো চলে না, ভুলটি শেষ পর্যন্ত বয়ে যায়। দুটি ইনপুট নিয়ে ধাপে ধাপে দেখুন:

1auto r = parseAmount(in)2    .and_then(checkLimit)3    .transform(addFee);4if (r) std::println("total {}", *r);5else   std::println("error: {}", r.error());
প্রতিটি ধাপের পর বাক্সে কী আছে
in
parseAmount
and_then
transform
  1. ইনপুট "500". parseAmount সফল — বাক্সে মান 500.
  2. and_then. বাক্সে মান আছে, তাই checkLimit(500) ডাকা হলো। সীমার মধ্যে, তাই সেটিও মান 500 সহ একটি expected ফেরত দিল। (সীমা ভাঙলে এখানেই বাক্সে ভুল ঢুকত।)
  3. transform আর প্রিন্ট। মান আছে, তাই addFee চলল: 500 থেকে 505. if (r) সত্য, প্রিন্ট হলো total 505. পুরো সফল পথে একটিও if লিখতে হয়নি।
  4. ইনপুট "12x". parseAmount ব্যর্থ — বাক্সে এবার মান নেই, আছে কারণ: not a number: 12x.
  5. Short-circuit. and_then দেখল বাক্সে ভুল — checkLimit ডাকলই না, একই ভুল নিয়ে নতুন বাক্স দিল। transform-ও একই কাজ করল, addFee চলেনি। ভুলটি পথের সব ধাপ পেরিয়ে অক্ষত পৌঁছাল।
  6. শেষে একবার সিদ্ধান্ত। if (r) মিথ্যা, তাই r.error() প্রিন্ট হলো। মাঝের কোনো ধাপকে ভুল নিয়ে ভাবতে হয়নি — ঠিক exception-এর মতো সুবিধা, কিন্তু সব কিছু রিটার্ন মানে, চোখের সামনে, আর throw-এর খরচ ছাড়া।

std::optional-ও (পরের পর্বে বিস্তারিত) C++23 থেকে একই and_then, transform, or_else পেয়েছে। পার্থক্য: optional শুধু বলে “নেই”, expected বলে “নেই, কারণ এটা”। কারণ যদি ডাকার মানুষের কোনো কাজে না লাগে (যেমন map-এ একটি নাম খোঁজা), optional যথেষ্ট।

“মান অথবা ভুল” টাইপের ধারণা অনেক পুরোনো — Haskell-এর Either, পরে Rust-এর Result. C++-এ এটিকে জনপ্রিয় করেন Andrei Alexandrescu, ২০১২ সালে “Systematic Error Handling in C++” নামের একটি আলোচনায় একটি Expected<T> টাইপ দেখিয়ে। standard-এর প্রস্তাবটি (P0323, Vicente Botet আর JF Bastien) বহু বছর ধরে বহুবার সংশোধিত হয় — একটি বড় বিতর্ক ছিল, ভুল থাকা অবস্থায় মান চাইলে কী হবে। শেষে এটি C++23-এ যোগ হয়, আর একই সময়ে আরেকটি প্রস্তাবে যোগ হয় and_then/transform-এর মতো monadic ফাংশন।

std::error_code — এক ঝলক

C++11 থেকে <system_error>-এ আছে std::error_code: একটি সংখ্যা আর একটি “category” (কোন ব্যবস্থার ভুল — অপারেটিং সিস্টেম, নেটওয়ার্ক, নিজের লাইব্রেরি), সাথে মানুষের পড়ার মতো message(). এর সবচেয়ে চেনা ব্যবহার std::filesystem-এ, যেখানে প্রায় প্রতিটি ফাংশনের দুটি রূপ আছে — একটি exception ছোড়ে, অন্যটি error_code-এ ভুল লিখে দেয় (পর্ব ৪৪-এ বিস্তারিত):

std::error_code ec;
auto size = std::filesystem::file_size("data.csv", ec);   // throw করে না
if (ec) std::println("failed: {}", ec.message());

নিজের লাইব্রেরিতে std::expected<T, std::error_code> একটি জনপ্রিয় জোড়া — মান বা একটি মানসম্মত ভুলের কোড।

৮ · কোনটা কখন — সিদ্ধান্তের টেবিল

এখন আপনার হাতে চারটি যন্ত্র: assert, optional, expected আর exception — আর জরুরি অবস্থার জন্য terminate. প্রশ্ন একটাই, প্রথম অংশের দুটি প্রশ্ন: কে সামলাবে, আর কীভাবে জানবে। একটি কাজের টেবিল:

পরিস্থিতিযন্ত্রকারণ
নিজের কোডের যুক্তি ভাঙল, precondition ভাঙা (বাগ)assert; C++26 থেকে contractsসামলানোর কিছু নেই, কোড ঠিক করতে হবে; যত আগে থামা যায় তত ভালো
ব্যবহারকারীর ভুল ইনপুট, ভুল PIN, বাতিল লেনদেনstd::expectedপ্রায়ই ঘটে, ঠিক ডাকার মানুষ সামলায়, কারণ জানা দরকার
মান না-ও থাকতে পারে, কারণ গুরুত্বহীনstd::optional“নেই” স্বাভাবিক ফল, ভুল নয়
চালুর সময় config নেই, ডেটাবেসে সংযোগ নেইexceptionকালেভদ্রে ঘটে, অনেক স্তর ওপরের কেউ সামলায়
constructor রিসোর্স নিতে পারল নাexceptionconstructor-এর রিটার্ন মান নেই; অর্ধেক-তৈরি object-এর চেয়ে কোনো object না থাকা ভালো
মেমরি শেষbad_alloc-কে ওপরে যেতে দিনপ্রায় কেউই এটা স্থানীয়ভাবে সামলাতে পারে না
প্রতি সেকেন্ডে হাজারবার ঘটতে পারে এমন ব্যর্থতাstd::expectedthrow-এর খরচ এখানে অগ্রহণযোগ্য
প্রকল্পে -fno-exceptionsexpected / error_codeexception ব্যবহারই করা যায় না
destructor-এর ভেতরের ব্যর্থতাকখনো throw নয় — লগ করুন, আলাদা close() দিনunwinding-এর সময় দ্বিতীয় exception মানে terminate
ডেটা নষ্ট হয়ে গেছে, চালিয়ে যাওয়া বিপজ্জনকstd::terminate / abortভুল ডেটা নিয়ে চলার চেয়ে থেমে যাওয়া নিরাপদ

একটি বাস্তব প্রোগ্রামে এগুলো একসাথে থাকে। সার্ভারের main সাধারণত এমন দেখায় — ভেতরে expected আর exception দুটোই, আর সীমানায় একটি পরিষ্কার “শেষ রক্ষা”:

int main() {
    try {
        Config cfg = loadConfig("app.conf");   // না পেলে ConfigError ছোড়ে
        runServer(cfg);   // ভেতরে প্রতিটি অনুরোধ যাচাই হয় std::expected দিয়ে
    } catch (const ConfigError& e) {
        std::println(stderr, "config: {}", e.what());
        return 1;                                // স্পষ্ট বার্তা, স্পষ্ট exit code
    } catch (const std::exception& e) {
        std::println(stderr, "fatal: {}", e.what());
        return 2;
    }
}
পরীক্ষা ও ইন্টারভিউতে
  • Stack unwinding কী? — throw থেকে মিলে-যাওয়া catch পর্যন্ত পথে প্রতিটি frame ছেড়ে যাওয়ার সময় তাদের local object-এর destructor উল্টো ক্রমে চলা।
  • Exception কেন const reference দিয়ে ধরবেন? — কপি এড়াতে আর slicing রোধ করতে, যাতে derived টাইপের what() ঠিকঠাক চলে।
  • throw; আর throw e;-এর পার্থক্য? — প্রথমটি একই exception object আবার পাঠায়; দ্বিতীয়টি e-এর ঘোষিত টাইপে একটি নতুন কপি ছোড়ে, ফলে আসল টাইপ হারাতে পারে।
  • Destructor থেকে throw করা যায় না কেন? — destructor ডিফল্টভাবে noexcept, আর unwinding-এর সময় দ্বিতীয় exception বেরোলে std::terminate হয়।
  • Basic আর strong guarantee-এর পার্থক্য? — basic: ব্যর্থতায় leak নেই আর object বৈধ, মান বদলাতে পারে; strong: ব্যর্থতায় সব কিছু ঠিক আগের অবস্থায় থাকে।

৯ · AI-এর যুগে — ভুল সামলানোর ভুল ধরা

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

AI যা লেখেসমস্যাযা হওয়া উচিত
catch (std::exception e)কপি আর slicing — আসল টাইপ ও বার্তা হারায়catch (const std::exception& e)
catch (...) {} — খালিভুল গিলে ফেলে; ডেটা আধা-বদলানো অবস্থায় প্রোগ্রাম চলেনির্দিষ্ট টাইপ ধরুন; catch (...) শুধু সীমানায়, লগসহ
লুপ থামাতে বা “খুঁজে পাওয়া যায়নি” বোঝাতে throwexception দিয়ে স্বাভাবিক নিয়ন্ত্রণ — ধীর, পড়তে কঠিনরিটার্ন মান: optional, expected, break
প্রতিটি expected-এ সোজা .value() বা *rexpected-এর পুরো লাভ নষ্ট: হয় অপ্রত্যাশিত exception, নয়তো undefined behaviorif (r), value_or, and_then/or_else
throw new MyError(...)pointer ছোড়া হয় — reference-এর catch মেলে না, leakthrow MyError(...)
catch-এর ভেতরে throw e;নতুন কপি, টাইপ কেটে যায়throw;
destructor-এ এমন কাজ, যা throw করতে পারেunwinding-এর সময় terminateআলাদা close(); destructor-এ শুধু শেষ চেষ্টা
move constructor-এ noexcept নেই, বা অকারণে সবখানে noexceptvector move না করে কপি করে; অথবা ভুল প্রতিশ্রুতিতে terminatemove/swap-এ noexcept; বাকিতে ভেবে-চিন্তে

AI-কে কোড লিখতে বলার সময় ভুলের নকশা আগেই বলে দিন, আর প্রতিটি ব্যর্থতার পথ ব্যাখ্যা করতে বলুন:

আমি C++23 শিখছি (g++, -Wall -Wextra). একটি ছোট mobile wallet-এর মডেল লেখো: Account (balance, limit), parseAmount (লেখা থেকে টাকার অঙ্ক), আর transfer(from, to, amount). নিয়ম: ১. ভুল ইনপুট আর সীমা ভাঙা প্রত্যাশিত ব্যর্থতা — এগুলোর জন্য std::expected<T, E> ব্যবহার করো, E-তে একটি enum class আর বার্তা। ২. config ফাইল না পাওয়া ব্যতিক্রমী — std::runtime_error থেকে বানানো নিজের exception class দিয়ে throw করো। ৩. transfer strong exception guarantee দেবে: দ্বিতীয় ধাপ ব্যর্থ হলে প্রথম ধাপ ফিরিয়ে নেবে। ৪. catch সবসময় const reference দিয়ে; catch (...) শুধু main-এ, আর সেখানে বার্তা প্রিন্ট করে exit code দিয়ে বেরোবে। খালি catch ব্লক লিখবে না। ৫. কোনো expected-এ যাচাই ছাড়া .value() বা * লিখবে না। ৬. move constructor আর swap-এ noexcept লিখবে, অন্য কোথাও অকারণে নয়। কোডের পরে একটি তালিকা দাও: প্রতিটি ফাংশন কোন exception safety guarantee দেয়, আর কোন ব্যর্থতা কোন পথে (রিটার্ন মান না exception) ডাকার মানুষের কাছে পৌঁছায়।

AI-এর কোডে ভুল-সামলানো যাচাইয়ের চেকলিস্ট

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

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

প্র ০১C++-এ exception এল কেন, কারা নকশা করলেন, আর তাঁরা “ভুলের জায়গায় ফিরে গিয়ে আবার চেষ্টা” (resumption) বাদ দিয়ে কেন শুধু “থেমে ওপরে যাওয়া” (termination) বেছে নিলেন?

১৯৮০-র দশকের শেষে C++ দ্রুত বড় হচ্ছিল, আর Bjarne Stroustrup একটি সমস্যা স্পষ্ট দেখছিলেন। C-রীতির রিটার্ন কোড C++-এর নতুন অংশগুলোর সাথে খাপ খায় না। constructor-এর কোনো রিটার্ন মান নেই — তাহলে ফাইল খুলতে না পারা একটি constructor কীভাবে ব্যর্থতা জানাবে? a + b-এর মতো overloaded operator রিটার্ন মানে ভুলের সংকেত রাখবে কোথায়? আর একটি লাইব্রেরির লেখক জানেন না, তাঁর লাইব্রেরি কোন প্রোগ্রামে ব্যবহার হবে — তাই ভুল হলে কী করা উচিত, সে সিদ্ধান্ত লাইব্রেরি নিতে পারে না; খবরটা পাঠাতে হবে ব্যবহারকারীর কাছে, যত স্তর ওপরেই সে থাকুক।

Andrew Koenig আর Stroustrup মিলে C++-এর exception handling নকশা করেন; তাঁদের প্রবন্ধ “Exception Handling for C++” ১৯৯০ সালের দিকে প্রকাশিত হয়, আর একই সময়ে নকশাটি ভাষার রেফারেন্স ম্যানুয়াল (ARM)-এ জায়গা পায়। সব compiler-এ এটি আসতে আরও কয়েক বছর লাগে, আর ১৯৯৮ সালের প্রথম ISO standard-এ এটি পাকাপাকি অংশ হয়।

নকশার সবচেয়ে আলোচিত সিদ্ধান্ত ছিল resumption বনাম termination. resumption মডেলে ভুল সামলানোর কোড ঠিক করে দিতে পারত, তারপর throw-এর জায়গা থেকে কাজ আবার চলত — শুনতে আকর্ষণীয়। কিন্তু Stroustrup তাঁর বই The Design and Evolution of C++-এ লিখেছেন, যেসব বড় ব্যবস্থায় resumption ছিল (যেমন Xerox PARC-এর অভিজ্ঞতা), সেখানে বাস্তবে এটি খুব কমই সত্যিকারের কাজে লাগত, আর প্রায়ই ভুলভাবে ব্যবহার হতো। resumption-এর কোড দুটি দূরের জায়গাকে গোপনে জুড়ে দেয়: ওপরের ফাংশনকে নিচের ফাংশনের ভেতরের অবস্থা জানতে হয়, যা মডিউলের সীমানা ভেঙে দেয়। termination মডেলে প্রতিটি ফাংশন নিজের অবস্থা নিজে গুছিয়ে রাখে (destructor দিয়ে), আর ওপরের স্তর শুধু ফলাফল দেখে সিদ্ধান্ত নেয়।

এই সিদ্ধান্তের সাথেই এসেছিল RAII-এর গুরুত্ব। termination মানে মাঝপথে ফাংশন ছেড়ে যাওয়া, আর তখন রিসোর্স ফেরত দেওয়ার একমাত্র নির্ভরযোগ্য উপায় destructor. Java পরে finally বেছে নিয়েছে, C++ বেছে নিয়েছে destructor — যা প্রতিটি ব্যবহারে আলাদা করে লিখতে হয় না।

মূল উপলব্ধি: exception এসেছিল একটি নকশার সমস্যা থেকে — যে জানে ভুল হয়েছে আর যে জানে কী করতে হবে, তারা প্রায়ই আলাদা মানুষ। termination মডেল আর RAII মিলে সেই দুই মানুষের মধ্যে একটি পরিষ্কার, নিরাপদ বার্তার পথ বানিয়েছে।

প্র ০২পুরোনো C++-এ ফাংশনের পরে throw(A, B) লিখে বলা যেত কোন exception বেরোতে পারে। এই “dynamic exception specification” কেন ব্যর্থ হলো, আর কেন C++17-এ তা ভাষা থেকে সরানো হলো?

C++98-এ লেখা যেত void f() throw(std::bad_alloc); — অর্থাৎ “f থেকে শুধু bad_alloc বেরোতে পারে”। শুনতে Java-র checked exception-এর মতো, যেখানে compiler যাচাই করে প্রতিটি সম্ভাব্য exception হয় ধরা হয়েছে, নয়তো ঘোষিত। কিন্তু C++-এর dynamic exception specification compile-এর সময় প্রায় কিছুই যাচাই করত না। যাচাই হতো চলার সময়: ঘোষণার বাইরের কোনো exception বেরোলে runtime একটি ফাংশন ডাকত, যার নাম ছিল std::unexpected(), আর সেটি ডিফল্টভাবে প্রোগ্রাম বন্ধ করত।

এতে তিন দিক থেকে ক্ষতি। প্রথমত, কোনো নিরাপত্তা নেই — ভুলটা ধরা পড়ে শুধু তখন, যখন ব্যবহারকারীর সামনে প্রোগ্রাম মরে যায়। দ্বিতীয়ত, খরচ: চলার সময় যাচাই করতে compiler-কে ফাংশনের চারপাশে বাড়তি কোড বসাতে হতো, তাই “কম exception ঘোষণা” লিখলে প্রোগ্রাম আসলে ধীর হতে পারত। তৃতীয়ত — এবং সবচেয়ে মৌলিক — template-এর সাথে এটি অচল। একটি generic sort জানে না তার উপাদানের তুলনা-ফাংশন কী ছুড়বে; সে সৎভাবে কোনো তালিকা লিখতেই পারে না। Herb Sutter সহ অনেক বিশেষজ্ঞ বহু বছর ধরে পরামর্শ দিয়েছেন: খালি throw() ছাড়া এগুলো লিখবেন না।

বাস্তবে একটিমাত্র তথ্যই কাজে লাগত: “এই ফাংশন কিছুই ছোড়ে না” — কারণ সেটা দেখেই library দ্রুততর পথ বেছে নিতে পারে (যেমন vector-এর move করা)। তাই C++11 আনল noexcept: একটি হ্যাঁ/না প্রশ্ন, compile-এর সময় noexcept(expr) দিয়ে জিজ্ঞেস করা যায়, আর ভাঙলে সরাসরি terminate — প্রতিটি ফাংশনে বাড়তি যাচাইয়ের কোড ছাড়াই। একই সাথে dynamic specification deprecated হলো। C++17-এ তালিকাসহ রূপ throw(A, B) পুরোপুরি সরানো হলো, আর খালি throw() কিছুদিন noexcept-এর একটি পুরোনো বানান হিসেবে টিকে ছিল, C++20-এ সেটিও গেল। পুরোনো std::unexpected() ফাংশনও C++17-এ সরানো হয় — আর মজার ব্যাপার, ঠিক সেই নামটিই C++23-এ ফিরে এসেছে সম্পূর্ণ নতুন অর্থে: std::expected-এর ভুল মোড়ানোর class হিসেবে।

মূল উপলব্ধি: যে চুক্তি compiler যাচাই করতে পারে না আর template-এর জগতে সৎভাবে লেখা যায় না, তা নিরাপত্তার ভান মাত্র। C++ শেষ পর্যন্ত রেখেছে শুধু সেই একটি তথ্য, যা সত্যিই কাজে লাগে আর সস্তায় দেওয়া যায় — noexcept.

প্র ০৩“Zero-cost exception” মডেল আসলে কীভাবে কাজ করে, এর আগে কী ছিল, আর “zero-cost” কথাটি কতটা সত্যি?

প্রথম দিকের অনেক compiler exception বানাত C-এর setjmp/longjmp কৌশল দিয়ে (SJLJ নামে পরিচিত)। প্রতিটি try-তে ঢোকার সময়, এমনকি destructor-ওয়ালা প্রতিটি object তৈরির সময়, runtime একটি তালিকায় নিবন্ধন করত — “ভুল হলে এখানে ফিরে আসবে”। throw দ্রুত ছিল, কিন্তু দাম দিতে হতো প্রতিবার, কিছু না ছুড়লেও। যে কোড কখনো exception ছোড়ে না, সে-ও ধীর হতো — ঠিক এই কারণেই সেই সময়ে অনেকে exception বন্ধ রাখতেন।

আধুনিক “table-based” পদ্ধতি উল্টো চিন্তা থেকে আসে: যা কালেভদ্রে ঘটে, তার সব খরচ সেই বিরল মুহূর্তেই দাও। Linux-এ GCC আর Clang যে Itanium C++ ABI মেনে চলে, তাতে compiler প্রতিটি ফাংশনের জন্য টেবিল লেখে — কোন নির্দেশের ঠিকানা কোন cleanup বা কোন catch-এর আওতায়, আর stack frame কীভাবে উল্টাতে হয়। এগুলো থাকে প্রোগ্রামের আলাদা অংশে (যেমন .eh_frame), সাধারণ কোডের পথে নয়। throw হলে unwinder দুই ধাপে কাজ করে: প্রথমে শুধু খোঁজে, কোনো frame-এ মিলে-যাওয়া catch আছে কি না; পেলে দ্বিতীয় ধাপে আসলে frame-গুলো গুটিয়ে destructor চালায়। (প্রথম ধাপে কিছু না পেলে কোনো ঝামেলা ছাড়াই সরাসরি terminate করা যায় — এজন্যই অধরা exception-এ stack গুটানো হবে কি না, তা implementation-এর ওপর ছাড়া।) 64-bit Windows-এও একই ধরনের টেবিল-ভিত্তিক ব্যবস্থা আছে।

“Zero-cost” তাহলে কতটা সত্যি? সফল পথে বাড়তি নির্দেশ প্রায় শূন্য — এটুকু সত্যি। কিন্তু অন্য খরচ আছে। টেবিলগুলো প্রোগ্রামের আকার বাড়ায় — ছোট embedded যন্ত্রে এটা গুরুতর। যে ফাংশন ছুড়তে পারে, তাকে ঘিরে optimizer কিছু রূপান্তর করতে সাবধান থাকে। আর throw নিজে অনেক দামি: exception object-এর জন্য মেমরি, টেবিল খোঁজা, RTTI দিয়ে টাইপ মেলানো।

এর একটি গভীর ফল আছে: exception-এর খরচ অনিশ্চিত — একটি throw কতক্ষণ নেবে, তা stack কত গভীর আর কতগুলো destructor আছে তার ওপর নির্ভর করে। hard real-time ব্যবস্থায় (গাড়ির ব্রেক, বিমানের নিয়ন্ত্রণ) সবচেয়ে খারাপ সময় আগে থেকে জানা জরুরি, তাই সেখানে exception প্রায় নিষিদ্ধ।

মূল উপলব্ধি: zero-cost মডেল একটি বাজি — “ভুল কম হবে”। বাজি সত্যি হলে এটি রিটার্ন কোডের চেয়েও দ্রুত, কারণ সফল পথে কোনো যাচাই নেই। বাজি মিথ্যা হলে (প্রায়ই ঘটা ব্যর্থতায় exception) এটি সবচেয়ে ধীর পথ। তাই যন্ত্র বেছে নেওয়ার প্রশ্ন আসলে প্রশ্ন: ব্যর্থতা কত ঘন ঘন ঘটে?

প্র ০৪Herb Sutter-এর “deterministic exceptions” প্রস্তাব (P0709) কী চেয়েছিল, আর exception নিয়ে C++ সম্প্রদায়ের বিতর্ক এখনো কেন চলছে?

Herb Sutter বহু বছর C++ standard committee-র নেতৃত্বে ছিলেন। তিনি একটি অস্বস্তিকর বাস্তবতা তুলে ধরেন: C++ কার্যত দুটি উপভাষায় ভাগ হয়ে আছে। একদিকে exception-সহ “standard” C++, অন্যদিকে -fno-exceptions দিয়ে চলা বিশাল কোড — গেম, embedded, অনেক বড় কোম্পানির কোড। দ্বিতীয় দলের কাছে standard library-র বড় অংশ (যা ব্যর্থতায় throw করে) পুরোপুরি ব্যবহারযোগ্য নয়, তাই তারা নিজেদের বিকল্প লাইব্রেরি বানায়। একটি ভাষার এমন বিভাজন সবার জন্য ক্ষতিকর।

২০১৮ সালে তিনি প্রস্তাব দেন P0709, “Zero-overhead deterministic exceptions: Throwing values”. মূল ভাবনা: exception-কে heap-এর একটি object আর টেবিল-খোঁজার বদলে একটি সাধারণ, ছোট মান বানানো, যা ফাংশনের সাধারণ রিটার্ন পথ দিয়েই ফেরে — অনেকটা expected-এর মতো, কিন্তু compiler নিজে সামলায়, আর লেখার ধরন থাকে try/catch-এর মতো। এতে throw-এর খরচ হতো একটি সাধারণ রিটার্নের কাছাকাছি, আর সবচেয়ে গুরুত্বপূর্ণ — অনুমেয়, “deterministic”. প্রস্তাবে আরও একটি সাহসী যুক্তি ছিল: মেমরি শেষ হওয়া (bad_alloc) বেশিরভাগ প্রোগ্রামে বাস্তবে সামলানো যায় না, তাই সেটাকে exception না রেখে সরাসরি প্রোগ্রাম থামানো (fail-fast) উচিত — তাহলে standard library-র অনেক ফাংশন noexcept হয়ে যেত।

প্রস্তাবটি committee-তে অনেক আলোচনা পেয়েছে, কিন্তু এখনো standard-এ আসেনি। আপত্তিগুলো বাস্তব: পুরোনো ABI-এর সাথে সামঞ্জস্য, দুই রকম exception একসাথে থাকলে জটিলতা, আর bad_alloc নিয়ে প্রস্তাবটি সবাই মানেননি — কিছু ব্যবস্থা (যেমন বড় ডেটাবেস বা সার্ভার) মেমরি-সংকটে সত্যিই কিছু অংশ ছেড়ে দিয়ে চলতে চায়। Sutter পরে নিজের পরীক্ষামূলক “cppfront” প্রকল্পে এমন কিছু ভাবনা পরীক্ষা করেছেন।

এর মধ্যে C++ আংশিক উত্তর পেয়েছে: noexcept, std::expected, আর C++26-এর contracts (বাগের জন্য আলাদা ব্যবস্থা)। বিতর্কটা আসলে একটি পুরোনো টানাপোড়েন — exception কোডকে পরিষ্কার রাখে আর ভুল উপেক্ষা করা অসম্ভব করে, কিন্তু ব্যর্থতার পথগুলো চোখে দেখা যায় না আর খরচ অনিশ্চিত; রিটার্ন মান সব কিছু চোখের সামনে রাখে, কিন্তু কোডে যাচাইয়ের ভিড় বাড়ায়।

মূল উপলব্ধি: exception নিয়ে বিতর্ক “ভালো না খারাপ” নয়, বরং “কোন খরচ আপনার প্রকল্পের জন্য গ্রহণযোগ্য”। একজন ভালো C++ প্রকৌশলী দুটি পদ্ধতিই জানেন, আর প্রকল্পের নিয়ম আর ব্যর্থতার ধরন দেখে বেছে নেন।

প্র ০৫Rust-এ exception নেই, আছে Result; Go-তে ভুল ফেরে একটি দ্বিতীয় রিটার্ন মানে। এই ভাষাগুলো কেন এ পথে গেল, আর C++-এর exception + expected-এর সাথে তুলনা কেমন?

Go (২০০৯ সালে ঘোষিত) সিদ্ধান্ত নেয়, ভুল একটি সাধারণ মান। ফাংশন সাধারণত দুটি জিনিস ফেরত দেয় — ফল আর একটি error — আর ডাকার মানুষ সঙ্গে সঙ্গে লেখেন if err != nil. এর পক্ষে যুক্তি: ব্যর্থতার প্রতিটি পথ কোডে চোখে দেখা যায়, কোনো লুকানো লাফ নেই। বিপক্ষে: কোডের বড় অংশ জুড়ে একই যাচাই বারবার, আর compiler আপনাকে ভুল যাচাই করতে বাধ্য করে না (ফল ব্যবহার করলেই চলে)। Go-তে panic আর recover-ও আছে, কিন্তু রীতি অনুযায়ী তা শুধু সত্যিকারের অপ্রত্যাশিত অবস্থার জন্য।

Rust আরও এক ধাপ এগোয়। Result<T, E> ঠিক C++-এর expected<T, E>-এর মতো একটি টাইপ, কিন্তু ভাষা তিনটি বাড়তি সুবিধা দেয়। প্রথমত, Result-কে উপেক্ষা করলে compiler সতর্ক করে (এটি #[must_use] চিহ্নিত) — C++-এ এর কাছাকাছি হলো নিজের ফাংশনে [[nodiscard]] লেখা। দ্বিতীয়ত, ? অপারেটর: let v = parse(s)?; লিখলে ভুল হলে ফাংশন সেখানেই ভুলটি ফেরত দিয়ে বেরিয়ে যায় — exception-এর মতো সংক্ষিপ্ত, কিন্তু চোখে দেখা যায়। C++-এ এখনো এমন ভাষাগত চিহ্ন নেই, তাই আমরা and_then-এর শৃঙ্খল বা হাতে লেখা if (!r) return std::unexpected(r.error()); ব্যবহার করি। তৃতীয়ত, Rust বাগের জন্য আলাদা পথ রাখে — panic!, যা stack গুটিয়ে বা সরাসরি abort করে থামায় — ঠিক আমাদের “বাগ বনাম প্রত্যাশিত ব্যর্থতা” ভাগের মতো।

Java ভিন্ন পরীক্ষা করেছিল: checked exception, যেখানে প্রতিটি সম্ভাব্য exception ফাংশনের স্বাক্ষরে ঘোষণা করতে হয় আর compiler যাচাই করে। বাস্তবে অনেকে খালি catch লিখে নিয়ম এড়াতেন, আর পরের অনেক ভাষা (C#, Kotlin) এ ধারণা নেয়নি।

C++-এর অবস্থান মাঝামাঝি, আর সেটাই এর শক্তি ও জটিলতা। ব্যতিক্রমী, দূরে-সামলানো ব্যর্থতার জন্য exception — যা Rust আর Go-তে নেই বলে তাদের প্রতিটি স্তরে ভুল হাতে হাতে তুলতে হয় (Rust-এ ? দিয়ে সংক্ষেপে)। প্রত্যাশিত ব্যর্থতার জন্য expected — যা Result-এরই আত্মীয়। কিন্তু C++ বেছে নেওয়ার দায়িত্ব দেয় আপনাকে, আর একটি কোডবেসে দুই রীতি এলোমেলোভাবে মিশলে বিভ্রান্তি হয়।

মূল উপলব্ধি: সব ভাষাই একই সমস্যা সমাধান করছে — ব্যর্থতার খবর সঠিক মানুষের কাছে পৌঁছানো, উপেক্ষা করা কঠিন করা, আর বাগকে স্বাভাবিক ব্যর্থতা থেকে আলাদা রাখা। পার্থক্য শুধু কোন দিকটি ভাষা জোর করে, আর কোনটি আপনার শৃঙ্খলার ওপর ছাড়ে।

অনুশীলন

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

  1. আউটপুট অনুমান: config_throw.cpp-এ শুধু একটি জিনিস বদলানো হলো — loadConfig-এর ভেতরে readFile-এর ডাককে try { ... } catch (const ConfigError&) { std::println("using defaults"); } দিয়ে মোড়ানো হলো। এখন পুরো আউটপুট কী?

    enter startServer, enter loadConfig, enter readFile, leave readFile, using defaults, leave loadConfig, server up, leave startServer. এবার মিলে-যাওয়া catch আছে loadConfig-এ, তাই শুধু readFile-এর frame গুটায়। loadConfig-এর g তখনো জীবিত (catch-এর পরে ফাংশন স্বাভাবিকভাবে শেষ হয়)। startServer জানতেই পারে না কিছু ভুল হয়েছিল, তাই server up আসে, আর main-এর catch চলে না।

  2. ভাগ করুন: প্রতিটির জন্য বলুন — assert, expected (বা optional), নাকি exception, আর কেন। (ক) ব্যবহারকারী টাকার ঘরে লিখলেন “abc” (খ) নিজের লুপে হিসাব করা index ভেক্টরের আকার পেরিয়ে গেল (গ) একটি ব্যাংকিং সার্ভার চালু হওয়ার সময় এনক্রিপশন key-এর ফাইল পেল না (ঘ) একটি ফোনবুকে নাম খুঁজে পাওয়া গেল না (ঙ) একটি ফাংশনের নথিতে লেখা “count কখনো ঋণাত্মক হবে না”, কিন্তু এসেছে −৩.

    (ক) std::expected — প্রত্যাশিত, প্রায়ই ঘটে, ব্যবহারকারীকে কারণ বলতে হবে। (খ) assert — এটা নিজের বাগ, কোড ঠিক করতে হবে। (গ) exception — চালুর সময় একবার, স্থানীয়ভাবে কিছু করার নেই, main পর্যন্ত গিয়ে স্পষ্ট বার্তায় বন্ধ হওয়া উচিত। (ঘ) std::optional — “নেই” একটি স্বাভাবিক ফল, কারণ বলার দরকার নেই। (ঙ) precondition ভাঙা — ডাকার মানুষের বাগ, তাই assert বা contract. (যদি মানটি বাইরের ইনপুট থেকে আসে, তাহলে সীমানায় আগে যাচাই করে expected দিন — তখন ভেতরের ফাংশনে precondition নিশ্চিন্তে ধরা যায়।)

  3. বাগ শিকার: একটি AI এই কোড দিয়েছে — try { pay(); } catch (std::exception e) { } আর অন্য জায়গায় if (bad) throw new PayError("x"); আর ~Wallet() { db.flush(); }, যেখানে flush() ব্যর্থ হলে throw করে। কয়টি সমস্যা পান?

    চারটি। (১) by value ধরা — কপি আর slicing; const std::exception& লিখুন। (২) catch ব্লক খালি — ভুল চুপচাপ গিলে ফেলা হচ্ছে; অন্তত লগ করুন, বা এখানে না ধরে ওপরে যেতে দিন। (৩) throw new — pointer ছোড়া হচ্ছে, যা catch (const std::exception&) ধরে না, আর memory leak; লিখুন throw PayError("x");. (৪) destructor-এ throw-করতে-পারা flush() — destructor noexcept, তাই ব্যর্থ হলেই terminate; আলাদা flush()/close() দিন আর destructor-এ try/catch দিয়ে শুধু শেষ চেষ্টা করুন।

  4. নিজে লিখুন (কোড): parse_amount.cpp-এ একটি নতুন ধাপ যোগ করুন — checkRound, যা ১০-এর গুণিতক নয় এমন অঙ্ক বাতিল করে ("not multiple of 10: ...")। শৃঙ্খলে checkLimit-এর পরে বসান, আর ইনপুট তালিকায় "505" যোগ করুন। চালানোর আগে অনুমান করুন "505" আর "12x"-এর জন্য কী আসবে।

    যেমন: checkLimit-এর মতোই একটি ফাংশন, যার ভেতরে if (v % 10 != 0) return std::unexpected(...); আর তারপর return v;. শৃঙ্খলে .and_then(checkLimit)-এর ঠিক পরে .and_then(checkRound) বসান। ফল: 505 -> error: not multiple of 10: 505. "12x"-এর ফল আগের মতোই not a number: 12x — কারণ প্রথম ধাপেই ভুল, তাই checkRound ডাকাই হয় না। লক্ষ করুন, নতুন ধাপ যোগ করতে main-এর প্রিন্টের কোডে কিছুই বদলাতে হয়নি।

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

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

ক্রমC++-এ main একটি try-এর ভেতর থেকে f-কে ডাকে, f ডাকে g-কে, আর g একটি exception ছোড়ে; শুধু main-এ মিলে-যাওয়া catch আছে। ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. g-এর ভেতরে exception object তৈরি হয়ে ছোড়া হয়
  2. g-এ মিলে-যাওয়া catch খোঁজা হয় — নেই
  3. g-এর local object-দের destructor চলে
  4. f-এর local object-দের destructor চলে
  5. main-এর catch ব্লক চলে
  6. catch শেষে exception object ধ্বংস হয়

C++-এ একটি ConfigError (std::runtime_error থেকে বানানো) ছোড়া হয়েছে। নিচের কোন catch সবচেয়ে ভালো?

  • catch (std::exception e)
  • catch (const std::exception& e)
  • catch (std::exception* e)
  • catch (...)

C++-এ একটি catch ব্লকের ভেতরে নাম ছাড়া শুধু throw; লিখলে কী হয়?

  • সবসময় std::terminate হয়
  • ধরা exception-এর একটি নতুন কপি ছোড়া হয়
  • একই exception object আসল টাইপসহ আবার ওপরে যায়
  • একটি খালি std::exception ছোড়া হয়

C++-এ একটি noexcept ফাংশনের ভেতর থেকে একটি exception বেরিয়ে আসতে চাইলে কী ঘটে, যদিও ডাকার জায়গায় মিলে-যাওয়া catch আছে?

  • std::terminate ডাকা হয়, প্রোগ্রাম বন্ধ হয়
  • ডাকার জায়গার catch স্বাভাবিকভাবে ধরে
  • Compile error, প্রোগ্রাম তৈরিই হয় না
  • exception চুপচাপ উপেক্ষিত হয়, ফাংশন স্বাভাবিকভাবে ফেরে

C++23-এ std::expected<int, std::string> r = std::unexpected("bad"); চলার পর r.value() ডাকলে কী হয়?

  • 0 ফেরত দেয়
  • undefined behavior
  • "bad" ফেরত দেয়
  • std::bad_expected_access exception ছোড়ে

একটি C++ ফাংশন strong exception guarantee দেয়। ফাংশনটি মাঝপথে exception ছুড়লে কোনটি নিশ্চিত?

  • ফাংশনটি কখনো exception ছোড়ে না
  • সব object ঠিক ডাকার আগের অবস্থায় থাকে
  • কোনো leak নেই, তবে মান বদলে যেতে পারে
  • প্রোগ্রাম নিরাপদে বন্ধ হয়ে যায়

C++23-এ r একটি std::expected, যার ভেতরে ভুল আছে। r.and_then(f).transform(g) চললে কী ঘটে?

  • f ভুলটি নিয়ে ডাকা হয়, তারপর g
  • bad_expected_access exception ছোড়া হয়
  • f আর g কোনোটিই চলে না, ফলাফলে একই ভুল থাকে
  • ভুল মুছে গিয়ে একটি খালি মান আসে

শব্দকোষ

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

মনে রাখুন

  1. আগে নকশা: প্রতিটি ব্যর্থতায় ঠিক করুন কে সামলাবে আর সে কীভাবে জানবে। বাগ → assert; প্রত্যাশিত → expected/optional; ব্যতিক্রমী → exception.
  2. throw খবর ওপরে নেয়, পথে destructor চলে। std::exception-এর পরিবার থেকে টাইপ বানান, value হিসেবে ছুড়ুন, const& দিয়ে ধরুন, আবার পাঠাতে throw;.
  3. catch (...) শুধু সীমানায়, খালি কখনো নয়। destructor থেকে কখনো ছুড়বেন না; constructor ব্যর্থ হলে ছুড়ুন।
  4. তিন প্রতিশ্রুতি: nothrow, strong (commit-or-rollback, copy-and-swap), basic. noexcept ভাঙলে std::terminate; move আর swap-এ noexcept দিন।
  5. std::expected<T, E> (C++23): মান বা কারণ; আগে যাচাই, তারপর ব্যবহার; and_then/transform/or_else প্রথম ভুলেই থামে। throw দামি, তাই প্রায়ই-ঘটা ব্যর্থতায় expected.

পরের পর্বে: “মান থাকতে পারে, না-ও পারে” — std::optional; “কয়েকটি টাইপের যেকোনো একটি” — std::variant; আর “যেকোনো কিছু” — std::any. expected-এর এই তিন আত্মীয় কখন কোনটি, আর কেন নিরাপদ।

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

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