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

constructor ও destructor — object-এর জন্ম আর বিদায়

Constructors and Destructors — How an Object Begins and Ends

Taught in Bangla, this lesson covers object creation and destruction in C++: constructors that guarantee valid objects, member initializer lists, construction and destruction order, delegating constructors, = default and = delete, explicit, brace initialization and the most vexing parse, and destructors.

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

  • constructor আসলে কী নিশ্চয়তা দেয় — invariant, default আর parameterized constructor
  • member initializer list কেন body-তে assignment-এর চেয়ে ভালো, আর initialization order-এর ফাঁদ
  • default member initializer, delegating constructor, = default, = delete আর explicit
  • brace initialization, most vexing parse, aggregate আর C++20-এর designated initializer
  • যাচাই করা constructor, destructor-এর ক্রম, আর AI-এর লেখা constructor-এ কী কী ভুল খুঁজবেন

১ · জন্মের মুহূর্ত থেকেই বৈধ — constructor-এর আসল কাজ

আগের পর্বে আপনি class বানিয়েছেন: কিছু data member, কিছু member function, আর private দিয়ে ভেতরের ডেটা লুকানো। কিন্তু একটি প্রশ্ন বাকি থেকে গেছে। একটি Date class-এর ভেতরে দিন, মাস আর বছর আছে। object তৈরি হওয়ার ঠিক পরের মুহূর্তে এই তিনটি সংখ্যার মান কী? কেউ যদি ভুলে মান না বসায়, বা বসায় ৩১ ফেব্রুয়ারি?

জাতীয় পরিচয়পত্রের কথা ভাবুন। নির্বাচন কমিশন আধা-তৈরি কার্ড কারও হাতে দেয় না — “নাম পরে লিখে নেবেন, জন্মতারিখ ফাঁকা থাকল।” তথ্য যাচাই হয়, তারপর সম্পূর্ণ, বৈধ একটি কার্ড ছাপা হয়। আর তথ্য ভুল হলে (জন্মতারিখ ৩১ ফেব্রুয়ারি) কার্ড ছাপাই হয় না। একবার হাতে পেলে আপনি নিশ্চিত — এই কার্ডে ভুলভাল কিছু নেই। C++-এর constructor ঠিক এই কাউন্টার।

পর্ব ২০-এ দেখেছেন, প্রতিটি ভালো class-এর একটি invariant থাকে — এমন নিয়ম, যা object-এর পুরো জীবনে সত্য থাকতে হবে। Date-এর invariant: “মাস ১ থেকে ১২, দিন সেই মাসের দিনসংখ্যার মধ্যে।” member function-গুলো এই নিয়ম রক্ষা করে। কিন্তু রক্ষা করার আগে নিয়মটা প্রতিষ্ঠা করতে হয় — আর সেটাই constructor-এর কাজ।

মূল ধারণা

constructor object-কে তার প্রথম মুহূর্ত থেকেই বৈধ করে; destructor তার শেষ মুহূর্তে পরিষ্কার করে। মাঝখানের পুরো জীবনটা তাই সবসময় “বৈধ অবস্থায়” কাটে। এমন কোনো সময় নেই, যখন object আছে অথচ অর্ধেক তৈরি।

default আর parameterized constructor

constructor একটি বিশেষ member function: নাম ঠিক class-এর নামে, কোনো return type নেই (এমনকি void-ও নয়), আর object তৈরির সময় compiler নিজেই তাকে ডাকে। যে constructor কোনো argument ছাড়াই ডাকা যায়, সেটি default constructor. যে constructor argument নেয়, তাকে সাধারণভাবে বলে parameterized constructor. পর্ব ৯-এর function overloading-এর নিয়মে একটি class-এ একাধিক constructor থাকতে পারে:

class Date {
    int d, m, y;
public:
    Date() : d(1), m(1), y(2000) {}                       // default constructor
    Date(int day, int month, int year)                    // parameterized
        : d(day), m(month), y(year) { /* যাচাই — অংশ ৭-এ */ }
};

Date a;               // default constructor: 01/01/2000
Date b(16, 12, 1971); // parameterized: বিজয় দিবস
Date c{26, 3, 1971};  // একই, কিন্তু {} দিয়ে — অংশ ৬-এ কেন ভালো

একটি গুরুত্বপূর্ণ নিয়ম: আপনি যদি class-এ কোনো constructor না লেখেন, compiler নিজে একটি default constructor বানিয়ে দেয়। কিন্তু একটিও constructor লিখলে (যেমন শুধু তিন-argument-এরটি), compiler আর default constructor বানায় না। তখন Date a; লিখলে compile error. এটা ইচ্ছাকৃত: আপনি যদি বলেন “Date বানাতে দিন-মাস-বছর লাগবেই”, তাহলে compiler চুপচাপ একটি ফাঁকা Date বানানোর পথ খোলা রাখবে কেন?

constructor আর destructor হলো special member function-দের সদস্য — এমন ফাংশন, যা দরকার হলে compiler নিজে তৈরি করতে পারে। মোট ছয়টি: default constructor, destructor, copy constructor, copy assignment, move constructor আর move assignment. প্রথম দুটি এই পর্বে; বাকি চারটি পরের পর্বে, Rule of 0/3/5-এর সাথে।

২ · member initializer list — body-তে assignment নয়

উপরের কোডে constructor-এর মাথার পরে একটি : আর তারপর d(day), m(month), y(year) দেখেছেন। এর নাম member initializer list. নতুনরা (আর অনেক AI টুলও) প্রায়ই এভাবে লেখেন:

Date(int day, int month, int year) {
    d = day;      // এটা initialization নয় — assignment
    m = month;
    y = year;
}

সাধারণ int-এর ক্ষেত্রে ফলাফল একই মনে হয়, কিন্তু ভেতরের ঘটনা আলাদা। C++-এর নিয়ম হলো: constructor-এর body-র { শুরু হওয়ার আগেই সব member তৈরি হয়ে যায়। initializer list-এ যা বলা আছে, সেভাবে; যা বলা নেই, সেটি তার নিজের default উপায়ে (std::string হলে ফাঁকা string, int হলে অনির্ধারিত আবর্জনা মান)। body-তে = লেখা মানে তৈরি-হয়ে-যাওয়া একটি member-এর মান পরে বদলানো। এই পার্থক্য তিন জায়গায় বড় হয়ে ওঠে। ধাপে ধাপে দেখুন:

1class Student {2    const int roll;3    std::string name;4public:5    // A: body-তে assignment6    Student(int r, const std::string& n) {7        roll = r;8        name = n;9    }10    // B: initializer list11    Student(int r, const std::string& n)12        : roll(r), name(n)13    {}14};
member-দের অবস্থা
roll
name
name-এর কাজ
  1. A — body-তে ঢোকার আগেই। { পর্যন্ত পৌঁছানোর আগেই compiler সব member বানিয়ে ফেলে। list-এ কিছু বলা নেই, তাই name হলো ফাঁকা string, আর roll মান ছাড়াই। কিন্তু roll একটি const — যার মান জন্মের সময়েই দিতে হয়। compiler এখানেই error দেয়।
  2. A — const-এ assignment. body-তে roll = r; মানে একটি তৈরি-হয়ে-যাওয়া const-এর মান বদলানো। এটা নিষেধ, তাই compile error. একই সমস্যা reference member-এর: reference জন্মের সময় কারও সাথে বাঁধতে হয়, পরে বাঁধা যায় না।
  3. A — দ্বিগুণ কাজ। name = n; চলে, কিন্তু তার আগেই একটি ফাঁকা string বানানো হয়ে গেছে। ফলে কাজ হলো দুবার: আগে বানানো, পরে বদলানো। ছোট string-এ পার্থক্য নগণ্য; কিন্তু member যদি বড় vector বা এমন টাইপ হয়, যার default constructor-ই নেই, তখন এই পথ হয় ধীর, নয়তো একেবারে অসম্ভব।
  4. B — initializer list. এবার একই constructor, কিন্তু মান দেওয়া হচ্ছে :-এর পরে। এর মানে “এই member-কে এই মান দিয়েই তৈরি করো”।
  5. B — জন্মের মুহূর্তেই সঠিক মান। roll জন্ম থেকেই 42, তাই const-এর কোনো সমস্যা নেই। name সরাসরি n-এর কপি হিসেবে তৈরি — একটিই কাজ। reference member বা default constructor-হীন member-ও এভাবে ঠিকঠাক initialize হয়।
  6. B — body ফাঁকা। body-তে আর কিছু লেখার নেই। body থাকে সেই কাজের জন্য, যা “মান বসানো” নয় — যেমন invariant যাচাই, লগ লেখা, বা কোনো হিসাব। নিয়মটি মনে রাখুন: মান দেওয়া list-এ, বাকি কাজ body-তে।

default member initializer — ডিফল্ট মান class-এর ভেতরেই

পর্ব ১২-এ struct-এ দেখেছেন, member-এর পাশেই ডিফল্ট মান লেখা যায়: এর নাম default member initializer. class-এও একই জিনিস চলে, আর অনেক constructor থাকলে এটি দারুণ কাজে আসে:

class Account {
    std::string owner;
    long long balance = 0;      // ডিফল্ট: শূন্য টাকা
    bool locked{false};         // {} দিয়েও লেখা যায়
public:
    Account() = default;                                    // owner "", balance 0, locked false
    explicit Account(std::string o) : owner(std::move(o)) {} // balance আর locked ডিফল্ট মানে
    Account(std::string o, long long b) : owner(std::move(o)), balance(b) {} // balance-এর ডিফল্ট উপেক্ষিত
};

নিয়মটা সরল: কোনো constructor-এর initializer list যদি একটি member-এর কথা বলে, সেটাই চলে; না বললে default member initializer চলে; সেটিও না থাকলে member তার নিজের default উপায়ে তৈরি হয়। ফলে int বা pointer-এর মতো member-এর পাশে সবসময় একটি ডিফল্ট মান লিখে রাখা ভালো অভ্যাস — তাহলে কোনো constructor ভুলে গেলেও আবর্জনা মান থাকে না।

৩ · তৈরি আর ধ্বংসের ক্রম

একটি object-এর ভেতরে অন্য object থাকলে (পর্ব ২-এর composition), কে আগে জন্মায়, কে আগে বিদায় নেয়? এই ক্রম C++ standard ঠিক করে দেয়, আর এটা জানা থাকলে অনেক সূক্ষ্ম বাগ আগেই ধরতে পারবেন। একটি রিকশা বানানোর কারখানা কল্পনা করুন — রিকশার ভেতরে চাকা, সিট আর হুড। প্রতিটি অংশের constructor আর destructor একটি লাইন প্রিন্ট করে:

#include <print>
#include <string>

struct Part {
    std::string name;
    explicit Part(const char* n) : name(n) { std::println("  make  {}", name); }
    ~Part() { std::println("  break {}", name); }
};

class Rickshaw {
    Part wheel{"wheel"};     // default member initializer
    Part seat;
    Part hood;
public:
    Rickshaw() : seat("seat"), hood("hood") { std::println("Rickshaw ready"); }
    ~Rickshaw() { std::println("Rickshaw scrapped"); }
};

int main() {
    Rickshaw r;
    std::println("riding...");
}

আউটপুট আসে: make wheel, make seat, make hood, তারপর Rickshaw ready, riding..., Rickshaw scrapped, আর শেষে break hood, break seat, break wheel. তিনটি জিনিস লক্ষ করুন। এক, member-রা তৈরি হয়েছে class-এ ঘোষণার ক্রমে, আর Rickshaw-এর constructor-এর body চলেছে সবার শেষে — কারণ body চলার সময় সব অংশ হাতের কাছে থাকা চাই। দুই, ধ্বংসের সময় উল্টো: আগে Rickshaw-এর destructor-এর body, তারপর member-রা ঘোষণার উল্টো ক্রমে। তিন, main-এ কোথাও destructor ডাকা হয়নি — r-এর scope শেষ হতেই compiler নিজে ডেকেছে।

এবার একটি ফাঁদ। ধরুন initializer list-এ ক্রম উল্টে লিখলেন: Rickshaw() : hood("hood"), seat("seat"). কোনটি আগে তৈরি হবে? নিজে অনুমান করে তারপর stepper চালান:

1class Rickshaw {2    Part wheel{"wheel"};3    Part seat;4    Part hood;5public:6    Rickshaw() : hood("hood"), seat("seat")   // ক্রম উল্টো!7    { std::println("Rickshaw ready"); }8    ~Rickshaw() { std::println("Rickshaw scrapped"); }9    // body শেষে member ধ্বংস: hood, seat, wheel10};11int main() {12    Rickshaw r;13    std::println("riding...");14}
r-এর ভেতরের অংশগুলো
wheel
seat
hood
r
  1. Compiler আগেই সতর্ক করে। -Wall দিলে g++ বলে: hood তৈরি হবে seat-এর পরে — আপনি list-এ যা-ই লিখুন। এটি -Wreorder সতর্কবার্তা। তারপর main-এ Rickshaw r; — object তৈরি শুরু।
  2. প্রথমে wheel. class-এ প্রথম ঘোষিত member wheel. list-এ তার কথা নেই, তাই তার default member initializer {"wheel"} ব্যবহার হলো।
  3. তারপর seat — list-এ দ্বিতীয় হলেও। ঘোষণার ক্রমে দ্বিতীয় seat, তাই এখনই সে তৈরি হলো। list-এ যে hood আগে লেখা, সেটা পুরোপুরি উপেক্ষিত।
  4. শেষে hood. ঘোষণার ক্রমে তৃতীয়। ভাবুন, hood-এর initializer যদি seat-এর মান ব্যবহার করত, list দেখে মনে হতো সমস্যা — কিন্তু আসলে ঠিক। উল্টোটা হলে (seat যদি hood-এর মান পড়ত) seat পড়ত এখনো-না-জন্মানো hood — undefined behavior. এজন্যই সতর্কবার্তাটি গুরুত্বপূর্ণ।
  5. সব অংশ তৈরি, এবার body. constructor-এর body চলে সবার শেষে। এই মুহূর্তে r সম্পূর্ণ আর বৈধ — এখান থেকে তার জীবন শুরু।
  6. ব্যবহার। r তার জীবন কাটাচ্ছে। এখানে যত member function ডাকা হোক, সব অংশ জায়গামতো আছে বলে নিশ্চিত থাকা যায়।
  7. scope শেষ — আগে destructor-এর body. }-এ পৌঁছাতেই ~Rickshaw()-এর body চলে। এখনো সব অংশ বেঁচে আছে, তাই body চাইলে তাদের ব্যবহার করতে পারে (যেমন “শেষ হিসাব” লেখা)।
  8. তারপর member-রা, ঘোষণার উল্টো ক্রমে। hood, seat, wheel. তৈরির ঠিক উল্টো — যাতে কোনো member ধ্বংসের সময় যার ওপর সে নির্ভর করে, সে এখনো বেঁচে থাকে। স্তূপের ওপরের থালা আগে নামানোর মতো।

নিয়মটি পাকা করে মনে রাখুন: member initialize হয় class-এ ঘোষণার ক্রমে, initializer list-এ লেখার ক্রমে নয়। তাই list-এ সবসময় ঘোষণার ক্রমেই লিখুন, আর -Wall চালু রাখুন (g++ আর clang-এ -Wreorder এর অংশ)। একটি member-এর initializer অন্য member-এর মান পড়লে, পড়া member-টি যেন class-এ আগে ঘোষিত হয়।

একই “উল্টো ক্রম” নিয়ম object-দের ক্ষেত্রেও। একটি ফাংশনে a, তারপর b তৈরি হলে ধ্বংস হয় আগে b, তারপর a — পর্ব ১৭-এর লাইব্রেরির বইয়ের মতো। global আর static object-ও প্রোগ্রাম শেষে তাদের তৈরির উল্টো ক্রমে ধ্বংস হয়। পর্ব ২৪-এ inheritance এলে আরও একটি স্তর যোগ হবে: base class-এর অংশ তৈরি হয় সবার আগে, member-দেরও আগে, আর ধ্বংস হয় সবার শেষে।

C জানলে

C-তে struct-এর জন্য constructor নেই; প্রথা হলো date_init(&d, 16, 12, 1971) আর date_free(&d)-এর মতো ফাংশন, যা ডাকতে ভুলে গেলে struct আবর্জনা মান নিয়ে বসে থাকে। C++-এ একই কাজ compiler নিজে, ঠিক সময়ে ডাকে — ভোলার সুযোগ নেই। designated initializer C99 থেকে C-তে আছে, কিন্তু C-তে ক্রম ইচ্ছামতো হতে পারে, আর C++20-এ ঘোষণার ক্রম মানতে হয়। C-এর দিকটা দেখতে C কোর্সের struct ও union পর্ব দেখুন।

৪ · delegating constructor, = default আর = delete

একটি constructor অন্যটিকে কাজ দেয়

একটি লগ ফাইলের class ভাবুন — একটি সার্ভার যেখানে প্রতিটি ঘটনা লিখে রাখে। কেউ শুধু ফাইলের নাম দেবে, কেউ নাম আর মোড (আগের লেখার পরে যোগ করা, নাকি মুছে নতুন শুরু), কেউ কিছুই দেবে না। তিনটি constructor-এ একই যাচাই আর ফাইল-খোলার কোড তিনবার লিখলে, একদিন একটিতে বদলাবেন আর বাকি দুটিতে ভুলে যাবেন। C++11 থেকে এর সমাধান delegating constructor: একটি constructor তার initializer list-এ অন্য একটি constructor ডাকে।

class LogFile {
    std::ofstream out;
    std::string path;
public:
    LogFile(const std::string& p, bool append)              // আসল কাজ এখানেই
        : out(p, append ? std::ios::app : std::ios::trunc), path(p)
    {
        if (!out) throw std::runtime_error("cannot open " + p);
        std::println("open {}", path);
    }
    explicit LogFile(const std::string& p) : LogFile(p, true) {}   // ডিফল্ট মোড: append
    LogFile() : LogFile("app.log") {}                              // ডিফল্ট ফাইল
    ~LogFile() { std::println("close {}", path); }                 // out নিজে ফাইল বন্ধ করে

    LogFile(const LogFile&) = delete;              // একই ফাইলের দুই মালিক নয়
    LogFile& operator=(const LogFile&) = delete;
};

এখানে LogFile() কাজ দেয় এক-argument-এর constructor-কে, আর সেটি দুই-argument-এর আসলটিকে। এই শৃঙ্খলে ঠিক কোনটি কখন চলে, দেখুন:

1LogFile() : LogFile("app.log")2{ std::println("default file"); }3explicit LogFile(const std::string& p) : LogFile(p, true)4{ std::println("default mode: append"); }5LogFile(const std::string& p, bool append)6    : out(p, append ? std::ios::app : std::ios::trunc), path(p)7{ std::println("open {}", path); }8~LogFile() { std::println("close {}", path); }910LogFile log;   // কোনো argument নেই
log-এর অবস্থা
কোন constructor
path
object
  1. argument ছাড়া object. LogFile log; — overload resolution বেছে নিল LogFile().
  2. প্রথম হাতবদল। LogFile()-এর initializer list-এ আছে LogFile("app.log"). তাই body চলার আগেই সে কাজটা অন্য constructor-কে দিল। খেয়াল করুন, delegating constructor-এর list-এ অন্য কোনো member লেখা যায় না — শুধু সেই একটি ডাক।
  3. দ্বিতীয় হাতবদল। এক-argument-এরটিও নিজে কিছু করে না, ডাকে LogFile(p, true). এই শেষ constructor-কে বলে target constructor.
  4. আসল কাজ — একবারই। target constructor-এর list-এ member-রা তৈরি হলো, ফাইল খুলল, body চলল। target constructor শেষ হওয়ামাত্র object “সম্পূর্ণ তৈরি” গণ্য হয়। এরপর কোনো delegating constructor-এর body exception ছুড়লে destructor চলবে।
  5. ফেরার পথে body-গুলো। এবার শৃঙ্খল উল্টো দিকে ফেরে: এক-argument constructor-এর body চলে।
  6. সবশেষে প্রথম জনের body. LogFile()-এর body সবার শেষে। ফলে আসল নিয়ম, যাচাই আর ফাইল-খোলা লেখা আছে ঠিক এক জায়গায় — বদলাতে হলে একটি জায়গাই বদলাবেন।

শৃঙ্খলে কখনো বৃত্ত বানাবেন না (A ডাকে B, B ডাকে A) — এটা ভুল প্রোগ্রাম, compiler সবসময় ধরতে পারে না। আর বেশিরভাগ ক্ষেত্রে default argument বা default member initializer দিয়েও একই ফল পাওয়া যায়; delegating constructor সবচেয়ে কাজের, যখন body-তে যাচাইয়ের মতো আসল কোড আছে।

= default — compiler-এর নিজের নিয়মে বানানো

অন্য constructor লিখলে compiler default constructor বানানো বন্ধ করে, মনে আছে? কিন্তু কখনো আপনি দুটোই চান — Account-এর মতো, যেখানে ফাঁকা Account-ও অর্থবহ, কারণ default member initializer আগেই ঠিক মান দিয়ে রেখেছে। তখন লিখুন Account() = default;. এটি একটি defaulted function: compiler-কে স্পষ্ট করে বলা, “নিজের নিয়মে এটা বানাও।” Account() {} লেখার চেয়ে এটা ভালো, কারণ compiler তখন class-টিকে আরও সরল (trivial) রাখতে পারে আর পাঠকও বোঝেন — এখানে বিশেষ কিছু নেই।

= delete — নিষেধ, compiler-এর মুখে

উপরের LogFile-এ copy constructor আর copy assignment-এর পরে = delete দেখেছেন। এটি একটি deleted function: ফাংশনটি আছে, কিন্তু ডাকলেই compile error. একই ফাইলের দুটি LogFile object থাকলে দুটিই একদিন ফাইল বন্ধ করত, তাই কপি নিষেধ — পর্ব ১৭-এর unique_ptr ঠিক এভাবেই কপি আটকায়। = delete যেকোনো ফাংশনে চলে। যেমন void setAge(int a);-এর পাশে void setAge(double) = delete; লিখলে setAge(25.7) আর চুপচাপ 25 হয়ে যায় না, error দেয়। (কপি আর move-এর পুরো নিয়ম পরের পর্বে।)

৫ · explicit — অজান্তে টাইপ বদল আটকানো

একটি argument নেওয়া constructor-এর একটি গোপন ক্ষমতা আছে: C++ তাকে implicit conversion-এর জন্য ব্যবহার করে। অর্থাৎ যেখানে Temperature দরকার, সেখানে একটি double দিলে compiler নিজে থেকেই Temperature(double) ডেকে রূপান্তর করে নেয়। এমন constructor-কে বলে converting constructor. কখনো এটা সুবিধাজনক (std::string s = "hi"; এভাবেই কাজ করে), কিন্তু নিজের টাইপে প্রায়ই বিপজ্জনক। একটি আবহাওয়া স্টেশনের কোডে কী হতে পারে, দেখুন:

1struct Temperature {2    double celsius;3    Temperature(double c) : celsius(c) {}      // explicit নেই4};5void checkHeat(Temperature t) { if (t.celsius > 40) alarm(); }6double h = sensor.humidity();   // 85 (শতাংশ)7checkHeat(h);                   // ভুল ভ্যারিয়েবল!8// ঠিক করা সংস্করণ:9explicit Temperature(double c) : celsius(c) {}10checkHeat(h);                   // এবার error11checkHeat(Temperature{36.5});   // উদ্দেশ্য চোখে দেখা যায়
কী ঘটছে
h
t.celsius
ফল
  1. একটি নিরীহ constructor. Temperature(double c) — একটি argument, explicit নেই। তাই এটি একটি converting constructor: compiler দরকার মনে করলে double-কে নিজে থেকেই Temperature বানাবে।
  2. অন্য অর্থের একটি সংখ্যা। সেন্সর থেকে আর্দ্রতা এল — ৮৫ শতাংশ। এটাও একটি double, কিন্তু অর্থ সম্পূর্ণ আলাদা।
  3. ভুল ভ্যারিয়েবল পাঠানো হলো। তাড়াহুড়োয় কেউ checkHeat(h) লিখলেন। compiler দেখে: Temperature লাগবে, এসেছে double, আর double থেকে Temperature বানানোর constructor আছে। তাই সে গোপনে Temperature(85.0) বানিয়ে দেয় — কোনো error নেই, কোনো warning নেই।
  4. ফল: ভুয়া সতর্কতা। ফাংশন ভাবল তাপমাত্রা ৮৫ ডিগ্রি, তাই alarm বাজল। এমন বাগ খুঁজে পেতে ঘণ্টা লাগে, কারণ ভুলের লাইনটি দেখতে একদম স্বাভাবিক।
  5. explicit যোগ করা হলো। এখন constructor শুধু স্পষ্টভাবে ডাকলে চলে — Temperature{36.5} বা Temperature(36.5). compiler আর গোপনে রূপান্তর করবে না। এই শব্দটি explicit specifier.
  6. একই ভুল এবার compile error. checkHeat(h) আর compile হয় না। ভুল ধরা পড়ল প্রোগ্রাম চালানোর আগেই — compiler-এর সময়ে ধরা ভুল সবচেয়ে সস্তা ভুল।
  7. সঠিক ব্যবহার স্পষ্ট। checkHeat(Temperature{36.5}) পড়েই বোঝা যায় “এটা তাপমাত্রা”। কেউ যদি checkHeat(Temperature{h}) লেখেন, ভুলটা অন্তত চোখের সামনে থাকে, code review-তে ধরা পড়ে।
মূল নিয়ম

একটি argument দিয়ে ডাকা যায় এমন প্রতিটি constructor-এ ডিফল্টভাবে explicit লিখুন (default argument থাকায় যেগুলো এক argument-এ ডাকা যায়, সেগুলোও)। শুধু তখনই বাদ দিন, যখন দুটি টাইপ সত্যিই “একই জিনিসের দুই রূপ” — যেমন const char* থেকে std::string. C++ Core Guidelines-ও একই পরামর্শ দেয়। C++20 থেকে শর্তসাপেক্ষ রূপও আছে: explicit(শর্ত), যা মূলত template-এ কাজে লাগে।

৬ · brace initialization, most vexing parse আর aggregate

কেন {} — uniform initialization

C++11-এর আগে object বানানোর উপায় ছিল কয়েক রকম: =, (), আর array-র জন্য {}. প্রতিটির নিয়ম আলাদা। C++11 আনল brace initialization — প্রায় সব জায়গায় {} চলে, তাই একে “uniform initialization”-ও বলা হয়। পর্ব ৫-এ এর একটি সুবিধা দেখেছেন: {} narrowing conversion আটকায়। int marks{87.5}; compile error, অথচ int marks = 87.5; চুপচাপ 87 বানায়। constructor-এর বেলাতেও একই — Date d{16.5, 12, 1971} error দেয়।

most vexing parse — দেখতে object, আসলে ফাংশন

এবার একটি বিখ্যাত ফাঁদ। নিচের লাইনটি দেখে মনে হয় default constructor দিয়ে একটি Timer বানানো হচ্ছে:

Timer t();          // ফাঁদ: এটা object নয়! "t নামের ফাংশন, যে Timer ফেরত দেয়"
t.start();          // error: request for member 'start' in 't', which is of non-class type 'Timer()'

Timer t1;           // ঠিক: default constructor
Timer t2{};         // ঠিক: {} দিয়ে, সবচেয়ে স্পষ্ট

কারণটা C থেকে আসা একটি ব্যাকরণ-নিয়ম: যে লাইনকে ঘোষণা (declaration) হিসেবে পড়া সম্ভব, compiler তাকে ঘোষণাই ধরে। Timer t(); হুবহু একটি ফাংশন ঘোষণার মতো, যেমন int rand();. এই বিভ্রান্তির নাম most vexing parse. আরও জটিল রূপে দুটি টাইপের নাম একসাথে এলে (Widget w(Gadget());) অভিজ্ঞ প্রোগ্রামারও ধোঁকা খান। {} কখনো ফাংশন ঘোষণা হতে পারে না, তাই brace initialization এই সমস্যা পুরোপুরি দূর করে।

{}-এর একটি ব্যতিক্রম জেনে রাখুন। যে টাইপের std::initializer_list নেওয়া constructor আছে (যেমন std::vector), সেখানে {} সেই constructor-কে অগ্রাধিকার দেয়। std::vector<int> v(10, 5); মানে দশটি 5, কিন্তু std::vector<int> v{10, 5}; মানে দুটি উপাদান: 10 আর 5. container-এ “আকার” দিতে চাইলে () ব্যবহার করুন।

aggregate আর designated initializer

সব টাইপের constructor লাগে না। যে struct শুধু কিছু মান একসাথে বহন করে — কোনো invariant নেই, সব member public — তাকে C++ বলে aggregate, আর তাকে সরাসরি {} দিয়ে member-ক্রমে মান দেওয়া যায়: aggregate initialization. C++20 থেকে member-এর নাম লিখেও দেওয়া যায় — designated initializer:

struct Delivery {                 // aggregate: কোনো constructor নেই, সব public
    std::string area;
    int parcels = 1;              // default member initializer চলে
    bool cashOnDelivery = false;
};

Delivery a{"Mirpur", 3, true};                       // ক্রম অনুযায়ী
Delivery b{.area = "Uttara", .cashOnDelivery = true}; // C++20: নাম ধরে; parcels = 1
// Delivery c{.cashOnDelivery = true, .area = "Banani"}; // error: ঘোষণার ক্রম মানেনি

designated initializer পড়তে সহজ: true কোন member-এর মান, তা আর অনুমান করতে হয় না। দুটি নিয়ম: নামগুলো ঘোষণার ক্রমেই লিখতে হবে (মাঝে বাদ দেওয়া চলে), আর নাম-দেওয়া ও নাম-ছাড়া মান মেশানো যায় না। আর একটি সূক্ষ্ম কথা: C++20 থেকে কোনো struct-এ যেকোনো constructor ঘোষণা করলেই (এমনকি = default দিলেও) সেটি আর aggregate থাকে না। সিদ্ধান্তের নিয়ম: struct-এর member-দের মধ্যে কোনো নিয়ম রক্ষা করতে হলে (যেমন Date-এর বৈধ তারিখ), constructor-সহ class বানান; শুধু মান বহন করলে aggregate-ই যথেষ্ট।

৭ · যাচাই করা constructor — অবৈধ object জন্মাতেই দেবেন না

এবার অংশ ১-এর প্রতিশ্রুতি রাখার পালা: একটি Date, যা ৩১ ফেব্রুয়ারি নিতে অস্বীকার করে। constructor-এর কোনো return value নেই, তাই “ব্যর্থ হলাম” জানানোর একমাত্র পরিষ্কার উপায় একটি exception ছোড়া। exception বের হলে object আর তৈরিই হয় না — কোনো ভুল Date কারও হাতে পৌঁছায় না।

#include <print>
#include <stdexcept>
class Date {
    int d, m, y;
    static int daysIn(int month, int year) {
        static constexpr int days[] = {31,28,31,30,31,30,31,31,30,31,30,31};
        bool leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
        return (month == 2 && leap) ? 29 : days[month - 1];
    }
public:
    Date(int day, int month, int year) : d(day), m(month), y(year) {
        if (m < 1 || m > 12 || d < 1 || d > daysIn(m, y)) throw std::invalid_argument("invalid date");
    }
    void show() const { std::println("{:02}/{:02}/{}", d, m, y); }
};
int main() {
    Date a(29, 2, 2024);  a.show();
    try { Date b(31, 2, 2025);  b.show(); }
    catch (const std::invalid_argument& e) { std::println("rejected: {}", e.what()); }
}

আউটপুট দুই লাইন: 29/02/2024 আর rejected: invalid date. ২০২৪ অধিবর্ষ, তাই ২৯ ফেব্রুয়ারি বৈধ। কিন্তু Date b(31, 2, 2025)-এর constructor যাচাইয়ে ব্যর্থ হয়ে exception ছুড়েছে, তাই b.show() কখনো চলেনি — b নামের কোনো object আদৌ জন্মায়নি। এর মানে, প্রোগ্রামের যেকোনো জায়গায় একটি Date হাতে পেলে আপনি যাচাই ছাড়াই ধরে নিতে পারেন সেটি বৈধ। প্রতিটি ফাংশনের শুরুতে বারবার “তারিখটা ঠিক তো?” পরীক্ষার দরকার নেই — পরীক্ষা হয়েছে একবার, জন্মের দরজায়।

একটি গুরুত্বপূর্ণ নিয়ম: constructor exception ছুড়লে সেই object-এর destructor চলে না — কারণ object তো কখনো সম্পূর্ণ হয়নি। তবে তার যে member-রা ততক্ষণে তৈরি হয়ে গেছে, তাদের destructor ঠিকই চলে (তৈরির উল্টো ক্রমে)। এজন্যই রিসোর্স রাখুন RAII member-এ (std::ofstream, std::vector, std::unique_ptr): constructor-এর মাঝপথে ব্যর্থ হলেও আগে নেওয়া রিসোর্স নিজে থেকে ফেরত যায়। constructor-এ খোলা new লিখে পরের লাইনে exception হলে সেই মেমরি leak হয়। exception-এর পুরো গল্প, আর exception ছাড়া ব্যর্থতা জানানোর বিকল্প (std::expected), পর্ব ৩৭-এ।

বাস্তবে

যেখানে exception ব্যবহার নিষেধ (কিছু embedded বা গেম প্রজেক্টে), সেখানে প্রচলিত বিকল্প একটি factory function: static std::optional<Date> Date::make(int d, int m, int y), যা যাচাই করে বৈধ হলে Date, না হলে ফাঁকা ফেরত দেয়, আর আসল constructor private থাকে। লক্ষ্য একই — অবৈধ Date কেউ হাতে পাবে না। (std::optional পর্ব ৩৮-এ।)

৮ · destructor — শেষ মুহূর্তের দায়িত্ব

destructor-এর নাম ~ আর class-এর নাম, কোনো parameter নেই, কোনো return type নেই, আর প্রতিটি class-এ ঠিক একটি। পর্ব ১৭-এ দেখেছেন এটি RAII-এর অর্ধেক: constructor রিসোর্স নেয়, destructor ফেরত দেয়। এখানে তিনটি নকশা-প্রশ্ন বাকি।

কখন destructor লিখবেন? প্রায় কখনোই না

উপরের LogFile-এর destructor শুধু একটি লাইন প্রিন্ট করে; ফাইল বন্ধ করে std::ofstream member-টি নিজেই, তার নিজের destructor-এ। Account, Date, Delivery — কারও destructor লাগেনি। এটাই Rule of Zero: রিসোর্স রাখুন RAII member-এ, তাহলে compiler-এর বানানো destructor-ই সব member-কে ঠিক ক্রমে ধ্বংস করবে। নিজে destructor লিখুন শুধু তখন, যখন class নিজেই একটি কাঁচা রিসোর্সের মালিক — যেমন C লাইব্রেরির একটি handle, যার জন্য কোনো RAII মোড়ক নেই।

ফাঁকা destructor লিখবেন না। ~Account() {} দেখতে নিরীহ, কিন্তু নিজে destructor ঘোষণা করলে (এমনকি ~Account() = default; লিখলেও) compiler আর move constructor আর move assignment নিজে বানায় না। ফলে যেখানে object সস্তায় সরানো যেত, সেখানে নিঃশব্দে কপি হয় — কোনো error নেই, শুধু প্রোগ্রাম ধীর। কেন এমন নিয়ম, পরের পর্বে বিস্তারিত।

destructor কখনো exception ছোড়ে না

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

destructor নিজে ডাকবেন না

r.~Rickshaw(); লেখা ভাষায় বৈধ, কিন্তু সাধারণ কোডে এটা প্রায় সবসময় ভুল: scope শেষে compiler আবার ডাকবে, ফলে একই object দুবার ধ্বংস — undefined behavior. destructor ডাকা compiler-এর কাজ। আগেভাগে রিসোর্স ছাড়তে চাইলে object-কে একটি ছোট { } block-এ রাখুন, বা close()/reset()-এর মতো ফাংশন দিন।

constructor আর destructor C++-এর সবচেয়ে পুরোনো বৈশিষ্ট্যগুলোর একটি। ১৯৭৯ সালে Bjarne Stroustrup যখন Bell Labs-এ “C with Classes” শুরু করেন, প্রথম দিকের সংস্করণেই object তৈরি ও ধ্বংসের সময় নিজে থেকে চলা ফাংশনের ধারণা ছিল — Simula ভাষার class থেকে অনুপ্রাণিত হয়ে। অনেক পরে, ২০১১ সালের C++11 standard এই পরিবারে বড় সংযোজন আনে: delegating constructor, = default, = delete, default member initializer, brace initialization, আর move constructor. explicit এসেছিল C++98-এর standard তৈরির সময়, implicit conversion-এর বাগ দেখে; C++11-এ এটি conversion operator-এও প্রযোজ্য হয়, আর C++20-এ এল explicit(শর্ত) ও designated initializer.

পরীক্ষা ও ইন্টারভিউতে
  • member কোন ক্রমে initialize হয়? — class-এ ঘোষণার ক্রমে, initializer list-এ লেখার ক্রম যা-ই হোক; ধ্বংস হয় উল্টো ক্রমে।
  • কখন initializer list বাধ্যতামূলক? — const member, reference member, default constructor-হীন member, আর (পর্ব ২৪-এ) base class-কে argument দিতে।
  • default constructor কখন compiler বানায় না? — class-এ অন্য যেকোনো constructor ঘোষণা করলে; ফিরিয়ে আনতে X() = default;.
  • explicit কেন? — এক-argument constructor-কে গোপন টাইপ-রূপান্তরে ব্যবহার আটকাতে।
  • constructor exception ছুড়লে destructor চলে? — সেই object-এর নয়, তবে ততক্ষণে তৈরি হওয়া member-দের destructor চলে।

৯ · AI-এর যুগে — constructor-এর ভুল ধরা

AI টুলের লেখা class প্রায়ই compile হয় আর চলে, কিন্তু constructor-এ পুরোনো দিনের অভ্যাস থেকে যায়। কারণ অনলাইনে প্রচুর পুরোনো টিউটোরিয়াল আছে, যেখানে body-তে assignment আর ফাঁকা destructor “স্বাভাবিক”। এই ভুলগুলো সাধারণত চোখে পড়ে না, তাই আপনাকে খুঁজতে জানতে হবে:

AI যা করেসমস্যাযা হওয়া উচিত
constructor-এর body-তে name = n; age = a;দ্বিগুণ কাজ; const/reference member-এ চলেই নাmember initializer list, ঘোষণার ক্রমে
এক-argument constructor-এ explicit নেইগোপন টাইপ-রূপান্তর, ভুল argument compile হয়ে যায়explicit ডিফল্ট
ফাঁকা destructor ~X() {} বা virtual ছাড়া অকারণেনিজে থেকে move বন্ধ হয়, নিঃশব্দে কপিdestructor লিখবেনই না (Rule of Zero)
int/pointer member-এর কোনো ডিফল্ট মান নেইকোনো constructor ভুলে গেলে আবর্জনা মানdefault member initializer: int count = 0;
constructor কোনো যাচাই করে না, বা bool init() আলাদাঅবৈধ বা অর্ধেক-তৈরি object সম্ভবconstructor-এ যাচাই করে exception, বা factory function
destructor-এ throw বা ঝুঁকিপূর্ণ কাজunwinding-এর সময় std::terminateআলাদা close(); destructor ভুল গিলে ফেলে
Timer t(); বা vector<int> v{n} দিয়ে আকারফাংশন ঘোষণা / ভুল constructorTimer t{};, vector<int> v(n)

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

আমি C++23 শিখছি (g++, -Wall -Wextra). একটি BusTicket class লেখো: যাত্রীর নাম, আসন নম্বর (১ থেকে ৪০), ভ্রমণের তারিখ আর ভাড়া (টাকা, শূন্যের বেশি)। নিয়ম: ১. সব member-এর মান member initializer list-এ দেবে, class-এ ঘোষণার ক্রমেই; body-তে assignment নয়। ২. constructor-এ invariant যাচাই করবে; ভুল হলে std::invalid_argument ছুড়বে, যাতে অবৈধ টিকিট তৈরিই না হয়। ৩. এক argument দিয়ে ডাকা যায় এমন প্রতিটি constructor explicit হবে। ৪. সংখ্যার member-দের default member initializer থাকবে। ৫. কোনো destructor লিখবে না, যদি না সত্যিই কাঁচা রিসোর্স থাকে — থাকলে কেন, তা বলো। ৬. main-এ একটি বৈধ আর একটি অবৈধ টিকিট বানিয়ে দেখাও। কোডের পরে বলো: প্রতিটি member কোন ক্রমে তৈরি হয়, আর কোন লাইনে কোন invariant রক্ষা হচ্ছে।

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

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

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

প্র ০১member initialize হয় class-এ ঘোষণার ক্রমে, initializer list-এর ক্রমে নয়। C++ কেন এমন নিয়ম বেছে নিল — list-এর ক্রম মেনে চললেই তো সহজ হতো?

উত্তর লুকিয়ে আছে destructor-এ। একটি class-এর constructor অনেকগুলো থাকতে পারে, কিন্তু destructor ঠিক একটি। আর destructor-এর দায়িত্ব member-দের তৈরির উল্টো ক্রমে ধ্বংস করা — কারণ পরে তৈরি হওয়া member আগে তৈরি হওয়া member-এর ওপর নির্ভর করতে পারে (ধরুন একটি Logger member একটি Connection member ব্যবহার করে)। নির্ভরশীলকে আগে সরিয়ে তারপর যার ওপর নির্ভর, তাকে সরাতে হয়।

এবার কল্পনা করুন, ক্রম ঠিক হতো initializer list দিয়ে। একটি constructor লিখল : a(...), b(...), আরেকটি লিখল : b(...), a(...). তাহলে একই class-এর দুটি object দুই ভিন্ন ক্রমে তৈরি হতো। destructor কোন ক্রমে ধ্বংস করবে? সে জানতই না object-টি কোন constructor দিয়ে বানানো। জানতে হলে প্রতিটি object-এর ভেতরে লুকিয়ে একটি বাড়তি তথ্য রাখতে হতো — “আমি কোন ক্রমে তৈরি” — আর destructor-কে চলার সময় সেটা পড়ে সিদ্ধান্ত নিতে হতো। এটা প্রতিটি object-এ মেমরি আর প্রতিটি ধ্বংসে সময়ের খরচ, যা C++-এর zero-overhead নীতির বিরোধী: যা ব্যবহার করেন না, তার দাম দেবেন না।

তাই নিয়মটি হলো একটি একক, স্থির ক্রম, যা compile-এর সময়েই জানা — class-এ ঘোষণার ক্রম। সব constructor সেই ক্রমে তৈরি করে, destructor তার উল্টোতে ধ্বংস করে। Stroustrup তাঁর The Design and Evolution of C++ বইয়ে এই ধরনের নকশা-সিদ্ধান্তের যুক্তি আলোচনা করেছেন। একই কারণে base class-এর ক্রমও নির্ধারিত হয় class-এর base তালিকার ক্রমে।

এই নিয়মের মূল্য হলো সেই ফাঁদ, যা এই পর্বে দেখেছেন: list দেখে মনে হয় এক ক্রম, আসলে আরেক। ভাষা এখানে একটি আপস করেছে — নিয়মটা কঠোর রেখেছে, কিন্তু compiler-কে সতর্ক করতে দিয়েছে। g++ আর clang-এ -Wreorder (যা -Wall-এর অংশ) ঠিক এটাই ধরে। ভুল ক্রমকে সরাসরি error বানালে বিপুল পরিমাণ পুরোনো কোড ভেঙে যেত, তাই standard এটিকে বৈধই রেখেছে। C++20-এর designated initializer-এ অবশ্য নতুন করে শুরু করার সুযোগ ছিল, তাই সেখানে ঘোষণার ক্রম না মানলেই error.

মূল উপলব্ধি: ভাষার অনেক “অদ্ভুত” নিয়ম আসলে অন্য একটি নিশ্চয়তা রক্ষার দাম। এখানে নিশ্চয়তাটি হলো — একটিমাত্র destructor, বাড়তি খরচ ছাড়াই, সবসময় সঠিক উল্টো ক্রমে পরিষ্কার করবে।

প্র ০২destructor থেকে exception ছোড়া কেন এত বিপজ্জনক, আর C++11 কেন সব destructor-কে ডিফল্টভাবে noexcept বানাল?

পর্ব ১৭-এ দেখেছেন, exception উড়লে প্রোগ্রাম catch খুঁজতে খুঁজতে stack গুটিয়ে আনে, আর পথে প্রতিটি local object-এর destructor চলে। এখন ধরুন সেই destructor-দের একজন নিজেও একটি exception ছুড়ল। তখন একসাথে দুটি exception “উড়ছে”। কোনটি catch-এ যাবে? প্রথমটি হারিয়ে গেলে আসল ভুলের খবর মুছে যায়; দ্বিতীয়টি হারিয়ে গেলে নতুন ভুল লুকিয়ে যায়; দুটিকে একসাথে বহন করার কোনো ব্যবস্থা C++-এর নেই। তাই standard-এর সিদ্ধান্ত অনেক আগে থেকেই কঠোর: unwinding চলার সময় destructor থেকে exception বের হলে std::terminate — প্রোগ্রাম বন্ধ।

C++11-এর আগেও এটা সত্য ছিল, কিন্তু স্বাভাবিক সময়ে (unwinding ছাড়া) destructor থেকে exception ছোড়া যেত। সমস্যা হলো, একই destructor কখনো স্বাভাবিক পথে চলে, কখনো unwinding-এ — লেখক আগে জানেন না। তার ওপর standard library-র প্রায় সব কিছু ধরে নেয় যে element-এর destructor exception ছোড়ে না। একটি std::vector ধ্বংসের সময় যদি মাঝের একটি element-এর destructor ছুড়ত, বাকি element-গুলোর কী হবে? অর্ধেক ধ্বংস হওয়া vector সামলানোর কোনো ভালো উত্তর নেই।

C++11 যখন noexcept চালু করল, তখন একটি পরিষ্কার সিদ্ধান্ত নেওয়া হলো: destructor (আর operator delete) ডিফল্টভাবে noexcept, যদি না কোনো member বা base-এর destructor স্পষ্টভাবে noexcept(false) হয়। ফলে কেউ অসাবধানে destructor-এ throw লিখলে, exception বের হতেই সরাসরি terminate. এটা কঠোর, কিন্তু অনুমেয়। সাথে একটি লাভও আছে: compiler জানে destructor ছুড়বে না, তাই অনেক জায়গায় বাড়তি exception-পথের কোড বাদ দিতে পারে, আর container-রা নিরাপদে move-ভিত্তিক দ্রুত পথ বেছে নিতে পারে।

তাহলে যে পরিষ্কারের কাজ ব্যর্থ হতে পারে, তার কী হবে? ফাইলের শেষ লেখা বা ডেটাবেসের commit ব্যর্থ হতে পারে। প্রতিষ্ঠিত রীতি হলো দুই স্তর: একটি স্পষ্ট close() বা commit(), যা ব্যর্থতা জানায় (exception বা return value দিয়ে), আর destructor, যা close() আগে না ডাকা হলে শেষ চেষ্টা করে এবং কোনো ভুল হলে নিঃশব্দে গিলে ফেলে বা লগ করে। std::fstream নিজেও এভাবে চলে — destructor-এ বন্ধ করার ব্যর্থতা আপনি জানতে পারেন না, তাই জরুরি ক্ষেত্রে আগে নিজে close() ডেকে অবস্থা দেখুন।

মূল উপলব্ধি: destructor হলো শেষ নিরাপত্তা-জাল; জাল নিজে ছিঁড়ে গেলে নিচে আর কিছু নেই। তাই destructor কখনো ব্যর্থতা ঘোষণা করে না — যে কাজের ফল জানা জরুরি, সেটি জীবনের শেষ মুহূর্তের আগেই, একটি সাধারণ ফাংশনে করুন।

প্র ০৩অনেক পুরোনো লাইব্রেরি আর কিছু style guide-এ constructor প্রায় ফাঁকা, আর আসল কাজ হয় একটি আলাদা init() ফাংশনে (two-phase initialization). এর পক্ষে-বিপক্ষে যুক্তি কী?

two-phase initialization মানে object তৈরি হয় দুই ধাপে: আগে constructor (যা প্রায় কিছুই করে না), তারপর bool init(...), যা আসল কাজ করে আর ব্যর্থ হলে false ফেরত দেয়। এর উৎস মূলত একটি বাস্তব সীমাবদ্ধতা: constructor-এর কোনো return value নেই, তাই ব্যর্থতা জানানোর একমাত্র পরিষ্কার উপায় exception. যেসব পরিবেশে exception ছিল না বা নিষিদ্ধ, সেখানে এই কৌশল জন্মায়।

ইতিহাসে উদাহরণ অনেক। Symbian OS-এর মোবাইল ফোন প্রোগ্রামিংয়ে “two-phase construction” ছিল আনুষ্ঠানিক নিয়ম, কারণ তখনকার compiler-এ C++ exception ব্যবহারযোগ্য ছিল না আর সিস্টেমের নিজস্ব ভুল-ব্যবস্থা ছিল। Microsoft-এর পুরোনো MFC লাইব্রেরিতে অনেক object-এর Create() ধাপ আলাদা। Google-এর C++ style guide দীর্ঘদিন exception ব্যবহার নিষেধ করেছে, আর পরামর্শ দিয়েছে constructor-এ ব্যর্থ হতে পারে এমন কাজ না রাখতে — দরকারে factory function বা Init(). আরেকটি প্রচলিত কারণ: constructor-এর ভেতরে virtual ফাংশন ডাকলে derived class-এর সংস্করণ চলে না (পর্ব ২৫-এ দেখবেন কেন), তাই কেউ কেউ সেই কাজ init()-এ সরান।

কিন্তু এর দাম বড়। constructor আর init()-এর মাঝখানে object আছে, অথচ বৈধ নয় — ঠিক সেই “অর্ধেক-তৈরি” অবস্থা, যা থেকে বাঁচতে constructor বানানো হয়েছিল। এখন প্রতিটি member function-কে শুরুতে দেখতে হয় “init হয়েছে তো?”, invariant দুর্বল হয়ে যায়, আর কেউ init() ডাকতে ভুললে বা তার false উপেক্ষা করলে বাগ — compiler কিছুই বলে না। const member বা reference member-ও ব্যবহার করা যায় না, কারণ সেগুলো পরে বসানো যায় না।

আধুনিক C++-এ তাই দুটি পরিষ্কার পথ সুপারিশ করা হয়। exception ব্যবহার করা গেলে: constructor-এ যাচাই করে ছুড়ুন — object হয় সম্পূর্ণ, নয় অস্তিত্বহীন। exception ব্যবহার করা না গেলে: constructor private রেখে একটি static factory function দিন, যা std::optional<T> বা C++23-এর std::expected<T, E> ফেরত দেয়। এতেও অর্ধেক-তৈরি object বাইরের কেউ কখনো দেখে না, আর ভুল উপেক্ষা করা কঠিন। C++ Core Guidelines-এও এই পরামর্শ আছে: constructor-এর পরে আলাদা init ডাকতে হয় এমন নকশা এড়ান।

মূল উপলব্ধি: two-phase initialization একটি সীমাবদ্ধতার সমাধান, আদর্শ নয়। প্রশ্নটা সবসময় একই রাখুন: “কেউ কি কোনোভাবে একটি অবৈধ object হাতে পেতে পারে?” উত্তর “হ্যাঁ” হলে নকশা আরও শক্ত করার সুযোগ আছে।

প্র ০৪“most vexing parse” নামটি কোথা থেকে এল, এই সমস্যার মূল কোথায়, আর C++11-এর brace initialization কি সত্যিই সব সমস্যার সমাধান?

নামটি দিয়েছেন Scott Meyers, তাঁর ২০০১ সালের বই Effective STL-এ — বইয়ের একটি অধ্যায়ের শিরোনামেই তিনি একে C++-এর “most vexing parse” (সবচেয়ে বিরক্তিকর ব্যাকরণ-বিশ্লেষণ) বলেছেন। তাঁর উদাহরণ ছিল একটি ফাইল থেকে সংখ্যা পড়ে list বানানো, যেখানে std::list<int> data(std::istream_iterator<int>(file), std::istream_iterator<int>()); দেখতে একটি list object, কিন্তু compiler একে পড়ে একটি ফাংশন ঘোষণা হিসেবে — data নামের ফাংশন, যে দুটি parameter নেয় আর list ফেরত দেয়। ঘোষণার লাইনে প্রোগ্রামার কোনো error পান না; দুর্বোধ্য error আসে পরে, যখন data-কে list হিসেবে ব্যবহার করতে যান।

মূল কারণ C++-এর C-উত্তরাধিকার। C-এর ঘোষণার ব্যাকরণ খুব নমনীয়: parameter-এর নামের চারপাশে অতিরিক্ত বন্ধনী চলে (int (x); বৈধ ঘোষণা), আর parameter-এর নাম বাদও দেওয়া যায়। ফলে C++-এ একই লেখা কখনো “object বানাও” আর কখনো “ফাংশন ঘোষণা” — দুইভাবেই পড়া যায়। এই দ্ব্যর্থতা মেটাতে standard একটি নিয়ম দেয়: যা ঘোষণা হিসেবে পড়া সম্ভব, তা ঘোষণা। C-এর সাথে সামঞ্জস্য রাখতে এটা জরুরি ছিল, কিন্তু constructor-এর () সিনট্যাক্স যোগ হওয়ার পর ফাঁদ তৈরি হলো।

C++11-এর brace initialization এর জন্য একটি পরিষ্কার পথ খুলে দেয়: {} কখনো parameter তালিকা নয়, তাই Timer t{}; বা std::list<int> data{first, last};-এ কোনো দ্ব্যর্থতা নেই। narrowing ধরা আর aggregate-এর সাথে একই সিনট্যাক্স — বাড়তি লাভ।

কিন্তু সমাধানটি নিখুঁত নয়, আর সমালোচনাও কম হয়নি। std::initializer_list-কে অগ্রাধিকার দেওয়ার নিয়মের জন্য std::vector<int> v{10, 5} আর v(10, 5) সম্পূর্ণ আলাদা অর্থ দেয়। আরও পুরোনো সংস্করণে auto x{5};-এর টাইপ নিয়েও বিভ্রান্তি ছিল, যা C++17-এ সংশোধন হয়। Meyers নিজেই তাঁর পরের বই Effective Modern C++-এ () আর {}-এর পার্থক্য নিয়ে একটি পুরো অধ্যায় লিখেছেন। তাই অনেক অভিজ্ঞ প্রোগ্রামারের বাস্তব নিয়ম: সাধারণত {}, কিন্তু container-এ আকার বা পুনরাবৃত্তি দিতে (). C++20 এমনকি aggregate-এর জন্য () দিয়ে initialization-ও অনুমোদন দিয়েছে, যাতে দুই সিনট্যাক্সের পার্থক্য কিছুটা কমে।

মূল উপলব্ধি: পুরোনো সামঞ্জস্য রক্ষার দাম কখনো কখনো নতুন সমস্যা। একটি ভাষা যখন কয়েক দশকের পুরোনো কোড ভাঙতে পারে না, তখন সমাধান আসে “নতুন পথ” যোগ করে — আর প্রোগ্রামারকে জানতে হয় কোন পথ কখন।

প্র ০৫Java, C# বা Python-এ finally ব্লক আছে, যা সবসময় চলে। C++-এ finally নেই কেন — আর যখন সত্যিই একবারের জন্য “বের হওয়ার সময় এটা করো” দরকার, তখন কী করবেন?

Stroustrup নিজের ওয়েবসাইটের FAQ-তে এই প্রশ্নের সরাসরি উত্তর দিয়েছেন: C++-এ finally-এর দরকার নেই, কারণ C++ RAII সমর্থন করে, আর RAII প্রায় সবসময় ভালো। তাঁর যুক্তিটা একটি সহজ হিসাব। ধরুন একটি প্রোগ্রামে একটি রিসোর্স-টাইপ (যেমন ফাইল) একশো জায়গায় ব্যবহার হয়। finally-এর পথে একশো জায়গায় একশোবার পরিষ্কারের কোড লিখতে হয় — আর প্রতিটি জায়গা ভুলের একটি সুযোগ। RAII-এর পথে পরিষ্কারের কোড লেখা হয় একবার, class-এর destructor-এ; একশো ব্যবহারকারী কিছুই লেখেন না। অর্থাৎ finally-এর খরচ বাড়ে ব্যবহারের সংখ্যার সাথে, RAII-এর খরচ বাড়ে রিসোর্স-টাইপের সংখ্যার সাথে — আর দ্বিতীয়টি অনেক কম।

আরও একটি পার্থক্য: finally “মনে রাখার” ওপর নির্ভর করে। একটি নতুন রিসোর্স যোগ হলে প্রোগ্রামারকে মনে করে finally-এ একটি লাইন যোগ করতে হয়। destructor-এ সেই দায়িত্ব compiler নেয়, আর destructor ধ্বংসের ক্রমও ঠিক রাখে — যা হাতে লেখা finally-তে প্রোগ্রামারকে নিজে সাজাতে হয়। মজার ব্যাপার, পরে GC-ভাষাগুলোও এই দিকেই এসেছে: Java-র try-with-resources, C#-এর using, Python-এর with — সবই আসলে রিসোর্সের সাথে একটি “বন্ধ করার” পদ্ধতি বেঁধে দেওয়া, অর্থাৎ RAII-এর কাছাকাছি।

তবু কখনো এমন একবারের কাজ থাকে, যার জন্য পুরো class লেখা বাড়াবাড়ি — যেমন একটি C লাইব্রেরির ফাংশন ডাকার পরে বের হওয়ার সময় আরেকটি “বন্ধ” ফাংশন ডাকা। এর জন্য C++ সম্প্রদায় একটি ছোট কৌশল তৈরি করেছে: scope guard — একটি সাধারণ RAII object, যে constructor-এ একটি lambda নেয় আর destructor-এ সেটি চালায়। এই ধারণা জনপ্রিয় হয় ২০০০ সালে Andrei Alexandrescu আর Petru Marginean-এর একটি প্রবন্ধে। আজ C++ Core Guidelines-এর সহায়ক লাইব্রেরি GSL-এ gsl::finally আছে, আর standard-এর জন্য scope_exit-এর প্রস্তাব Library Fundamentals TS-এ পরীক্ষামূলক হিসেবে আছে। অর্থাৎ C++-এর “finally”-ও আসলে destructor দিয়ে বানানো।

এই পর্বের ভাষায় বললে: destructor হলো C++-এর একমাত্র জায়গা, যা object-এর জীবন যেভাবেই শেষ হোক — স্বাভাবিক }, return, break বা exception — নিশ্চিতভাবে চলে। ভাষার পুরো রিসোর্স-নিরাপত্তা এই এক নিশ্চয়তার ওপর দাঁড়ানো, তাই আলাদা finally যোগ করা মানে একই কাজের দ্বিতীয়, দুর্বলতর পথ খোলা।

মূল উপলব্ধি: ভালো ভাষা-নকশা প্রতিটি সমস্যার জন্য নতুন কীওয়ার্ড আনে না; একটি শক্তিশালী নিয়ম দিয়ে অনেক সমস্যা মেটায়। C++-এ সেই নিয়ম — “destructor সবসময় চলে”।

অনুশীলন

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

  1. আউটপুট অনুমান: Part আগের মতো (constructor-এ make, destructor-এ break). একটি class Shop { Part shelf{"shelf"}; Part till{"till"}; public: Shop() { std::println("open"); } ~Shop() { std::println("closed"); } }; আর main-এ Shop a; { Shop b; }. পুরো আউটপুট কোন ক্রমে?

    a-এর জন্য: make shelf, make till, open. তারপর b-এর জন্য একই তিন লাইন। ভেতরের {} শেষে b ধ্বংস: closed, break till, break shelf. শেষে main-এর শেষে a-ও একইভাবে: closed, break till, break shelf. প্রতিটি object-এর ভেতরে member ঘোষণার ক্রমে তৈরি, উল্টো ক্রমে ধ্বংস; আর destructor-এর body চলে member-দের ধ্বংসের আগে.

  2. বাগ শিকার: class Range { int size; int lo, hi; public: Range(int l, int h) : lo(l), hi(h), size(hi - lo) {} }; — Range r(3, 10);-এ size কত হবে? কেন? ঠিক করুন।

    size ঘোষিত হয়েছে lo আর hi-এর আগে, তাই সেটিই প্রথমে initialize হয় — তখনো hi আর lo তৈরি হয়নি, তাদের মান আবর্জনা। ফলে size একটি অনির্ধারিত মান (undefined behavior), 7 নয়। -Wall দিলে g++ -Wreorder সতর্কতা দেয়। সমাধান: class-এ ঘোষণার ক্রম বদলান (int lo, hi; int size;), অথবা size(h - l) লিখে parameter থেকে হিসাব করুন — দ্বিতীয়টি আরও নিরাপদ।

  3. নকশা বেছে নিন: প্রতিটির জন্য বলুন — constructor-সহ class, নাকি aggregate struct? কারণসহ। (ক) একটি Rgb রং, তিনটি 0–255 মান, যেকোনো মিশ্রণ বৈধ (খ) একটি BankAccount, যার balance কখনো ঋণাত্মক হবে না (গ) একটি ExamResult, যার marks 0 থেকে 100 (ঘ) একটি ফাংশন থেকে একসাথে ফেরত আসা {count, average}.

    (ক) aggregate — যদি member-এর টাইপ std::uint8_t হয়, টাইপই সীমা রক্ষা করে, আলাদা নিয়ম নেই; Rgb{.r = 255, .g = 128, .b = 0} পড়তে সুন্দর। (খ) class — invariant আছে, constructor যাচাই করবে আর member private. (গ) class — 0–100 সীমা একটি invariant; aggregate হলে কেউ {150} লিখে ফেলবে। (ঘ) aggregate — শুধু দুটি মান একসাথে বহন, কোনো নিয়ম নেই; structured binding দিয়ে খুলতেও সুবিধা।

  4. নিজে লিখুন (কোড): উপরের valid_date.cpp-এ একটি delegating constructor যোগ করুন: Date(int day, int month), যা বছর হিসেবে 2026 ধরে তিন-argument-এর constructor-কে কাজ দেয়। তারপর main-এ Date c(29, 2); চেষ্টা করুন। কী হবে, আগে অনুমান করুন।

    যোগ করুন: Date(int day, int month) : Date(day, month, 2026) {}. (এক argument-এর নয়, তাই explicit জরুরি নয়।) Date c(29, 2); চালালে exception ছোড়া হবে, কারণ ২০২৬ অধিবর্ষ নয় — যাচাই হয়েছে target constructor-এ, আর delegating constructor সেই একই যাচাই বিনা পরিশ্রমে পেয়েছে। try-এর ভেতরে রাখলে rejected: invalid date আসবে; না রাখলে প্রোগ্রাম std::terminate দিয়ে বন্ধ হবে।

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

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

ক্রমএকটি class-এ member a তারপর b ঘোষিত, আর constructor ও destructor-এর body-তে একটি করে লাইন প্রিন্ট হয়। scope-এ একটি object তৈরি হয়ে scope শেষ হলো। ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. a তৈরি হয়
  2. b তৈরি হয়
  3. constructor-এর body চলে
  4. destructor-এর body চলে
  5. b ধ্বংস হয়
  6. a ধ্বংস হয়

C++-এ একটি class-এ member int x; int y; এই ক্রমে ঘোষিত, আর constructor লেখা : y(1), x(y + 1). কী ঘটে?

  • y আগে 1 হয়, তারপর x হয় 2
  • x আগে তৈরি হয়, অনির্ধারিত y পড়ে — undefined behavior
  • Compile error — ক্রম ভুল
  • compiler নিজের সুবিধামতো ক্রম বেছে নেয়

C++-এ কোন ধরনের member-কে অবশ্যই member initializer list (বা default member initializer)-এ মান দিতে হয়, constructor-এর body-তে assignment দিয়ে চলে না?

  • সাধারণ int member
  • std::string member
  • static member
  • const member আর reference member

C++-এ struct Kg { Kg(double v); }; void ship(Kg w); আছে। ship(2.5); compile হয়। Kg(double)-এর আগে কী লিখলে এটি compile error হবে?

  • explicit
  • constexpr
  • noexcept
  • inline

C++-এ Timer t(); লাইনটি আসলে কী ঘোষণা করে?

  • default constructor দিয়ে বানানো একটি Timer object
  • শূন্য মানে initialize করা একটি Timer object
  • t নামের একটি ফাংশন, যা কোনো argument নেয় না আর Timer ফেরত দেয়
  • এটি সবসময় compile error

C++-এ একটি constructor-এর body-র মাঝখানে exception ছোড়া হলো, আর তার আগে দুটি member সম্পূর্ণ তৈরি হয়ে গিয়েছিল। কী ঘটে?

  • object-এর destructor চলে, যা সব member পরিষ্কার করে
  • object-এর destructor চলে না, কিন্তু তৈরি হওয়া দুটি member-এর destructor চলে
  • কোনো destructor চলে না, member-দের রিসোর্স leak হয়
  • প্রোগ্রাম সঙ্গে সঙ্গে std::terminate দিয়ে বন্ধ হয়

C++-এ একটি class-এ শুধু ~Widget() {} (ফাঁকা destructor) যোগ করলে কোন পরিবর্তনটি ঘটে?

  • member-রা আর নিজে থেকে ধ্বংস হয় না
  • class আর কপি করা যায় না
  • compiler আর move constructor নিজে বানায় না — move-এর জায়গায় কপি হয়
  • কিছুই বদলায় না, এটা শুধু শৈলী

শব্দকোষ

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

মনে রাখুন

  1. constructor invariant প্রতিষ্ঠা করে: object জন্ম থেকেই বৈধ; অবৈধ মান এলে exception — object তৈরিই হয় না।
  2. মান দিন initializer list-এ, class-এ ঘোষণার ক্রমেই; member তৈরি হয় ঘোষণার ক্রমে, ধ্বংস হয় উল্টো ক্রমে। সংখ্যার member-এ default member initializer রাখুন।
  3. এক-argument constructor-এ explicit; একই কোড বারবার না লিখে delegating constructor; অবাঞ্ছিত কাজ = delete, compiler-এর সংস্করণ চাইলে = default.
  4. {} দিয়ে initialize করুন — most vexing parse আর narrowing থেকে বাঁচবেন; container-এ আকার দিতে (). শুধু মান বহনকারী struct-এ aggregate আর C++20-এর designated initializer.
  5. destructor লিখবেন খুব কম (Rule of Zero), ফাঁকা destructor কখনো নয়, আর destructor কখনো exception ছোড়ে না।

পরের পর্বে: একটি object কপি করলে আসলে কী কপি হয়? কেন ফাঁকা destructor move বন্ধ করে দেয়, আর কখন শূন্য, তিন বা পাঁচটি special member function নিজে লিখতে হয় — copy, move আর Rule of 0/3/5.

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

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