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

inheritance — উত্তরাধিকার, is-a সম্পর্ক

Inheritance — Modelling the "is-a" Relationship

Taught in Bangla, this lesson explains inheritance in C++: is-a relationships, base and derived classes, protected access, construction and destruction order, name hiding, object slicing, public, protected and private inheritance, multiple inheritance and the diamond, final, and the Liskov principle.

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

  • Inheritance কোন সম্পর্ক বোঝায়, আর কখন সেটি ব্যবহার করা উচিত নয়
  • Base ও derived class-এর syntax, কী উত্তরাধিকারে আসে, আর object-এর ভেতরে base অংশ কোথায় থাকে
  • protected, constructor-destructor-এর ক্রম, base constructor ডাকা, আর using Base::Base
  • Name hiding আর object slicing — কেন ঘটে, কীভাবে ধরবেন, আর পর্ব ২৫-এর polymorphism কেন reference বা pointer চায়
  • public/protected/private inheritance, multiple inheritance ও diamond, final, Liskov নীতি, আর AI-এর লেখা class hierarchy যাচাই

১ · Inheritance আসলে কী বলে — “এটি এক ধরনের …”

পর্ব ২-এ আপনি সমস্যাকে বস্তু আর তাদের দায়িত্বে ভাঙতে শিখেছেন, আর পর্ব ২০ থেকে ২৩-এ নিজের টাইপ বানাতে শিখেছেন — class, constructor, copy আর operator. এবার প্রশ্ন আসে টাইপদের মধ্যে সম্পর্ক নিয়ে। বাস্তব জগতে অনেক জিনিস অন্য জিনিসের বিশেষ রূপ। সঞ্চয়ী হিসাব এক ধরনের ব্যাংক হিসাব। ম্যানেজার এক ধরনের কর্মচারী। বৃত্ত এক ধরনের আকৃতি। এই “এক ধরনের” সম্পর্ককে ইংরেজিতে বলে is-a relationship, আর C++-এ সেটি লেখার উপায় হলো inheritance (উত্তরাধিকার)।

একটি ব্যাংকের কথা ভাবুন। প্রতিটি হিসাবের কিছু সাধারণ নিয়ম আছে — হিসাবধারীর নাম, ব্যালান্স, টাকা জমা আর তোলা। এখন ব্যাংক নতুন একটি “সঞ্চয়ী হিসাব” চালু করল, যেখানে বছর শেষে সুদ যোগ হয়। কর্মকর্তারা পুরো নিয়মের বই নতুন করে লেখেন না। তাঁরা লেখেন: “সঞ্চয়ী হিসাব হলো একটি সাধারণ হিসাব, তার সাথে সুদের হার আর বছর শেষে সুদ যোগ।” কাউন্টারের কর্মী জানেন, যে ফর্ম দিয়ে সাধারণ হিসাবে টাকা জমা হয়, সেই একই ফর্ম দিয়ে সঞ্চয়ী হিসাবেও হয়। সাধারণ হিসাবের প্রতিটি প্রতিশ্রুতি সঞ্চয়ী হিসাবও রাখে। এটাই inheritance-এর আসল অর্থ।

শেষ বাক্যটি গুরুত্বপূর্ণ। নতুনরা প্রায়ই inheritance-কে ভাবেন “কোড কপি না করে ধার নেওয়ার যন্ত্র”। একটি class-এ দরকারি কয়েকটি ফাংশন আছে, তাই সেখান থেকে inherit করে নিলাম — এই ভাবনা থেকেই খারাপ নকশার জন্ম। কোড পুনর্ব্যবহার inheritance-এর একটি ফল, উদ্দেশ্য নয়। আসল উদ্দেশ্য একটি প্রতিশ্রুতি: derived টাইপের object-কে base টাইপের জায়গায় নিশ্চিন্তে বসানো যাবে। কোড ধার নিতে চাইলে বেশিরভাগ সময় ভালো উপায় composition — অর্থাৎ অন্য object-কে নিজের সদস্য হিসেবে রাখা (“has-a”, যেমন গাড়ির একটি ইঞ্জিন আছে)। এই তুলনা পর্ব ২৭-এ গভীরে দেখবেন।

is-a → inheritance

ম্যানেজার এক ধরনের কর্মচারী। Circle এক ধরনের Shape. SavingsAccount এক ধরনের Account. যেখানে বড়টি চলে, ছোটটিও চলে।

class Manager : public Employee

has-a → composition

গাড়ির একটি ইঞ্জিন আছে। অর্ডারের কয়েকটি পণ্য আছে। গাড়ি এক ধরনের ইঞ্জিন নয়, তাই এখানে সদস্য রাখুন।

Engine engine;

“কোড দরকার” → সাধারণত composition

একটি Stack-এর ভেতরে vector-এর কাজ দরকার, কিন্তু Stack এক ধরনের vector নয় — মাঝখান থেকে কেউ ঢুকিয়ে দিলে stack-এর নিয়ম ভাঙে।

std::vector<T> items;

এই পর্বে তিনটি বাস্তব উদাহরণ বারবার আসবে: একটি অফিসের Employee আর তার বিশেষ রূপ Manager ও Intern (বেতনের নিয়ম আলাদা); একটি ব্যাংকের Account আর SavingsAccount (সুদ সহ); আর জ্যামিতির Shape. আগের class-টিকে বলে base class (মূল বা পিতৃ class), আর তার থেকে তৈরি নতুনটিকে derived class (উদ্ভূত class). কয়েক স্তরের এই সম্পর্ক মিলে তৈরি হয় একটি class hierarchy.

২ · Base আর derived class — syntax আর কী উত্তরাধিকারে আসে

প্রথমে একটি সাধারণ কর্মচারী। পর্ব ২০-এর নিয়ম মেনে data private, আর বাইরের দুনিয়ার জন্য কয়েকটি public ফাংশন:

class Employee {
public:
    Employee(std::string name, int baseSalary)
        : name_(std::move(name)), base_(baseSalary) {}
    const std::string& name() const { return name_; }
    int baseSalary() const { return base_; }
private:
    std::string name_;
    int base_;          // টাকায় মাসিক মূল বেতন
};

class Manager : public Employee {             // Manager এক ধরনের Employee
public:
    Manager(std::string name, int base, int teamSize)
        : Employee(std::move(name), base), teamSize_(teamSize) {}
    int teamAllowance() const { return teamSize_ * 1000; }  // শুধু Manager-এর
private:
    int teamSize_;
};

class Manager : public Employee — কোলনের পরে public শব্দটি বলে এটি public inheritance: Employee-এর public অংশ Manager-এরও public অংশ। এখন একটি Manager object-এর ওপর name(), baseSalary() আর teamAllowance() — তিনটিই ডাকা যায়। Employee-এর কোড Manager-এ একটি লাইনও কপি করতে হয়নি।

কী উত্তরাধিকারে আসে, কী আসে না

base-এর জিনিসderived-এ কী হয়
data member (private সহ)সবই derived object-এর ভেতরে থাকে। কিন্তু private সদস্য derived-এর কোড সরাসরি ছুঁতে পারে না — base-এর public/protected ফাংশন দিয়ে যেতে হয়।
public ও protected member functionউত্তরাধিকারে আসে, derived নিজের মতো ব্যবহার করতে পারে।
constructorনিজে থেকে আসে না। derived-কে নিজের constructor লিখতে হয়, অথবা using Base::Base; দিয়ে নিয়ে আসতে হয় (অংশ ৪)।
destructorউত্তরাধিকারে আসে না, কিন্তু derived-এর destructor শেষ হলে base-এর destructor নিজে থেকেই চলে।
copy assignment (operator=)derived-এর নিজের (compiler-এর বানানো) operator= base-এরটিকে ঢেকে দেয়; সেটি ভেতরে base অংশের operator= ডাকে।
friendবন্ধুত্ব উত্তরাধিকারে আসে না। base-এর বন্ধু derived-এর বন্ধু নয়।

প্রথম সারিটা নতুনদের অবাক করে: private সদস্য derived object-এর ভেতরে আছে, অথচ derived ছুঁতে পারে না? হ্যাঁ — এবং এটাই ভালো। Employee-এর invariant (ধরুন, বেতন কখনো ঋণাত্মক নয়) রক্ষা করার দায়িত্ব Employee-এর। Manager যদি সরাসরি base_ = -500 লিখতে পারত, তাহলে সেই নিয়ম ভাঙার আরেকটি দরজা খুলে যেত। encapsulation শুধু বাইরের দুনিয়ার জন্য নয়, derived class-এর জন্যও।

এবার ভেতরের ছবিটা দেখুন। মেমরিতে একটি derived object আসলে কেমন দেখায়, আর একটি base reference তার কতটুকু দেখে — ধাপে ধাপে। (ঠিকানা আর ক্রম বোঝানোর জন্য সরল করা; বাস্তবে compiler padding যোগ করতে পারে।)

Account a{"Rina", 5000};একটি সাধারণ Account
a :  ┌──────────────────────────────┐
     │ owner   = "Rina"             │
     │ balance = 5000               │
     └──────────────────────────────┘
SavingsAccount s{"Sakib", 8000, 4.5};base অংশ + নিজের অংশ
s :  ┌──────────────────────────────┐
     │ Account অংশ (base subobject) │
     │   owner   = "Sakib"          │
     │   balance = 8000             │
     ├──────────────────────────────┤
     │ SavingsAccount-এর নিজের অংশ  │
     │   rate    = 4.5              │
     └──────────────────────────────┘
Account& r = s;derived থেকে base — নিজে থেকেই
s :  ┌──────────────────────────────┐
r ──▶│ Account অংশ                  │  ◀ r এই অংশটিকে দেখে
     │   owner   = "Sakib"          │
     │   balance = 8000             │
     ├──────────────────────────────┤
     │ rate    = 4.5                │  ◀ আছে, কিন্তু r-এর চোখে অদৃশ্য
     └──────────────────────────────┘
r.deposit(500); r.rate; // errorr-এর টাইপ Account — সে Account-এর কাজই জানে
r.deposit(500)   ✔  balance = 8500  (s-এর ভেতরেই বদলাল)
r.rate           ✘  error: 'class Account' has no member named 'rate'

r কোনো কপি নয় — s-এর base অংশের আরেকটি নাম।
তাই যে ফাংশন Account& নেয়, সে যেকোনো SavingsAccount-ও নিতে পারে।
  1. একটি সাধারণ object. Account-এর দুটি সদস্য: মালিকের নাম আর ব্যালান্স। মেমরিতে এরা পাশাপাশি থাকে।
  2. Derived object = base অংশ + নিজের অংশ। SavingsAccount object-এর ভেতরে একটি পুরো Account বসানো আছে — একে বলে base subobject. তার পরে আসে derived-এর নিজের সদস্য rate. তাই sizeof(SavingsAccount) কখনো sizeof(Account)-এর চেয়ে ছোট নয়।
  3. Base reference — কপি ছাড়াই। Account& r = s; লিখতে কোনো cast লাগে না। একে বলে derived-to-base conversion: “প্রতিটি সঞ্চয়ী হিসাব একটি হিসাব”, তাই compiler নিজেই এটা মেনে নেয়। r তাকিয়ে থাকে s-এর ভেতরের Account অংশের দিকে। (উল্টো দিকে, Account থেকে SavingsAccount — নিজে থেকে হয় না, কারণ প্রতিটি হিসাব সঞ্চয়ী নয়।)
  4. যা দেখা যায়, তা টাইপ ঠিক করে। r দিয়ে Account-এর কাজ — যেমন deposit — করা যায়, আর বদল হয় আসল s-এর ভেতরেই। কিন্তু r.rate লিখলে compile error, কারণ r-এর টাইপ Account, আর Account-এ rate নেই। এই সুবিধাটাই পর্ব ২৫-এর polymorphism-এর ভিত্তি: একটি ফাংশন Account& নিয়ে সব ধরনের হিসাবের সাথে কাজ করতে পারে।
C জানলে

C-তে inheritance নেই, কিন্তু অভিজ্ঞ C প্রোগ্রামাররা ঠিক উপরের ছবিটা হাতে বানান: “base” struct-কে নতুন struct-এর প্রথম সদস্য করে রাখেন, যাতে নতুন struct-এর ঠিকানাকে base-এর pointer হিসেবে ব্যবহার করা যায়। GTK-এর GObject এই কৌশলে চলে, আর Linux kernel-ও struct-এর ভেতরে struct বসিয়ে (container_of দিয়ে) একই ধরনের কাজ করে। C++ একই মেমরি-ধারণা ভাষার ভেতরে এনে দিয়েছে — কাস্ট ছাড়া, টাইপ-নিরাপদভাবে। struct-এর মেমরি বিন্যাস নিয়ে গভীরে জানতে C কোর্সের struct ও union পর্ব দেখুন।

৩ · protected — পরিবারের জন্য খোলা দরজা, সাবধানে

পর্ব ২০-এ আপনি দুটি access specifier দেখেছেন: public (সবার জন্য) আর private (শুধু class নিজে)। inheritance তৃতীয়টি নিয়ে আসে — protected: class নিজে আর তার derived class-রা ছুঁতে পারে, বাইরের কেউ পারে না।

সদস্যclass নিজেderived class-এর কোডবাইরের কোড
publicহ্যাঁহ্যাঁহ্যাঁ
protectedহ্যাঁহ্যাঁনা
privateহ্যাঁনানা

শুনতে সুবিধাজনক — derived class-কে base-এর data সরাসরি ছুঁতে দিলে ঝামেলা কম। কিন্তু একটু থেমে ভাবুন। Account-এর balance যদি protected হয়, তাহলে এই প্রকল্পে ভবিষ্যতে যে কেউ যত derived class লিখবেন — StudentAccount, LoanAccount, কারও তাড়াহুড়োর TestAccount — প্রত্যেকেই balance সরাসরি বদলাতে পারবেন। “ব্যালান্স কখনো ঋণাত্মক নয়” নিয়মটি তখন আর এক জায়গায় রক্ষিত থাকে না; থাকে প্রতিটি derived class-এর সততার ওপর। protected data কার্যত সব derived class-এর জন্য public data, আর derived class-এর সংখ্যা আপনি নিয়ন্ত্রণ করতে পারেন না।

class Account {
public:
    void deposit(int taka) { if (taka > 0) balance_ += taka; }
    int balance() const { return balance_; }
protected:
    void addInterest(int taka) { balance_ += taka; }  // derived-দের জন্য নিয়ন্ত্রিত পথ
private:
    int balance_ = 0;                                  // data private-ই থাকল
};

class SavingsAccount : public Account {
public:
    void yearEnd() { addInterest(balance() * rate_ / 100); }  // ঠিক আছে
    // balance_ = -100;   // error: private — Account-এর নিয়ম রক্ষা পেল
private:
    int rate_ = 4;
};

লক্ষ্য করুন নকশাটি: data private, আর derived-দের যে বিশেষ সুবিধা দরকার, সেটি একটি protected ফাংশন হিসেবে। Account এখনো নিজের নিয়ম নিজে রক্ষা করছে; শুধু একটি নির্দিষ্ট, নামওয়ালা দরজা খোলা রেখেছে। এটাই ভালো রীতি।

মূল নিয়ম

data member প্রায় সবসময় private. derived class-এর জন্য কিছু খুলতে হলে protected ফাংশন দিন, data নয়। C++ Core Guidelines-ও একই পরামর্শ দেয়: protected data এড়িয়ে চলুন।

একটি সূক্ষ্ম নিয়ম, যা ইন্টারভিউতে আসে: derived class base-এর protected সদস্য ছুঁতে পারে শুধু নিজের (বা নিজের থেকে derived) টাইপের object-এর মাধ্যমে। SavingsAccount-এর ভেতরে অন্য একটি সাধারণ Account& other পেলে other.addInterest(100) লেখা যায় না। protected মানে “আমার পরিবারের নিজের অংশ”, অন্যের হিসাবে হাত দেওয়ার অনুমতি নয়।

৪ · তৈরি আর ধ্বংসের ক্রম — ভিত আগে, ছাদ পরে

একটি বহুতল ভবন বানাতে আগে ভিত, তারপর নিচতলা, তারপর ওপরের তলা। ভাঙার সময় উল্টো — আগে ছাদ, ভিত সবার শেষে। derived object-ও ঠিক এভাবে তৈরি হয়। কারণটা যুক্তিসংগত: derived-এর constructor চলার সময় সে base-এর ফাংশন ডাকতে পারে, তাই base অংশ তখন সম্পূর্ণ তৈরি থাকতেই হবে। আর ধ্বংসের সময় derived-এর destructor base-এর জিনিস ব্যবহার করতে পারে, তাই base অংশ তখনো বেঁচে থাকতে হবে।

base-এর constructor কীভাবে আর কোন যুক্তি দিয়ে ডাকা হবে, তা ঠিক করা হয় derived-এর constructor-এর member initializer list-এ — কোলনের পরের অংশে, যেমন : Account(std::move(o)), rate(r). সেখানে না লিখলে compiler base-এর default constructor ডাকার চেষ্টা করে; base-এর default constructor না থাকলে compile error. তিন স্তরের একটি hierarchy চালিয়ে দেখুন — Account থেকে SavingsAccount, তার থেকে ছাত্রদের জন্য বিশেষ StudentSavings:

#include <print>
#include <string>
#include <utility>

struct Account {
    std::string owner;
    explicit Account(std::string o) : owner(std::move(o)) { std::println("Account  ctor: {}", owner); }
    ~Account() { std::println("Account  dtor: {}", owner); }
};
struct Savings : Account {
    double rate;
    Savings(std::string o, double r) : Account(std::move(o)), rate(r) { std::println("Savings  ctor: rate {}%", rate); }
    ~Savings() { std::println("Savings  dtor"); }
};
struct StudentSavings : Savings {
    explicit StudentSavings(std::string o) : Savings(std::move(o), 5.5) { std::println("Student  ctor"); }
    ~StudentSavings() { std::println("Student  dtor"); }
};
int main() {
    StudentSavings s("Rina");
    std::println("--- using {} ---", s.owner);
}

আউটপুট আসে এই ক্রমে: Account ctor: Rina, Savings ctor: rate 5.5%, Student ctor, --- using Rina ---, তারপর Student dtor, Savings dtor, Account dtor: Rina. খেয়াল করুন, main-এ আমরা শুধু StudentSavings বানাতে বলেছি, অথচ প্রথম লাইনটি Account-এর। আর ধ্বংস হয়েছে আয়নার মতো উল্টো ক্রমে। struct ব্যবহার করেছি উদাহরণ ছোট রাখতে — struct-এ inheritance ডিফল্টভাবে public. আর s.owner সরাসরি পড়া গেল, কারণ owner দুই স্তর ওপরের Account থেকে উত্তরাধিকারে এসেছে।

একটা প্রশ্ন জাগে: StudentSavings-এর constructor-ই তো প্রথমে ডাকা হয়েছে, তাহলে তার লাইন শেষে কেন? কারণ constructor ডাকা আর তার body চলা এক জিনিস নয়। ডাকার পর আগে initializer list চলে, আর সেটি ওপরের স্তরের constructor ডাকে — body চলে সবার শেষে। ধাপে ধাপে দেখুন:

1StudentSavings s("Rina");                  // main2  StudentSavings(o) : Savings(o, 5.5)      // init list আগে3    Savings(o, r) : Account(o), rate(r)    // আবার init list4      Account(o) : owner(o) { println }    // সবার ওপরের স্তর5    Savings body   { println }6  StudentSavings body { println }7std::println("--- using ...");8}  // main শেষ: ~StudentSavings()9     → তারপর ~Savings()10     → সবশেষে ~Account()
s-এর তিনটি অংশ
Account অংশ
Savings অংশ
Student অংশ
  1. জায়গা নেওয়া হলো। main-এর stack-এ পুরো StudentSavings-এর জন্য একসাথে জায়গা রাখা হলো — তিনটি অংশেরই। কিন্তু এখনো কোনো অংশ তৈরি হয়নি।
  2. StudentSavings-এর constructor ডাকা হলো — কিন্তু body নয়। আগে তার initializer list চলে, আর সেখানে প্রথম কাজ base অংশ বানানো: Savings(o, 5.5) ডাকা।
  3. Savings-এর constructor — আবার initializer list. Savings-এরও একটি base আছে, তাই সে-ও নিজের body চালানোর আগে Account(o) ডাকে। আমরা hierarchy বেয়ে ওপরে উঠছি।
  4. সবার ওপরের স্তর তৈরি। Account-এর কোনো base নেই। তার owner সদস্য তৈরি হয়, body চলে — প্রথম লাইন প্রিন্ট হয়। ভিত দাঁড়াল।
  5. এক ধাপ নিচে নামা। Account শেষ, এবার Savings-এর বাকি initializer (rate(r)) আর তারপর তার body. দ্বিতীয় লাইন।
  6. Object সম্পূর্ণ। সবশেষে StudentSavings-এর body চলে। এই মুহূর্ত থেকে s একটি পূর্ণ StudentSavings — এর আগে সেটি “আংশিক” ছিল। নিয়ম: base আগে, তারপর derived-এর সদস্যরা (ঘোষণার ক্রমে), তারপর body.
  7. ব্যবহার। s-কে স্বাভাবিকভাবে ব্যবহার করা হলো। owner এসেছে দুই স্তর ওপর থেকে।
  8. ধ্বংস শুরু — নিচের তলা থেকে। main-এর }-এ s-এর জীবন শেষ। প্রথমে চলে সবচেয়ে derived class-এর destructor, অর্থাৎ ~StudentSavings. ছাদ আগে নামল।
  9. এক স্তর ওপরে। ~StudentSavings শেষ হতেই compiler নিজে ~Savings ডাকে। আপনাকে কোথাও “base destructor ডাকো” লিখতে হয়নি।
  10. ভিত সবার শেষে। সবশেষে ~Account — owner তখনো বেঁচে আছে, তাই “Rina” প্রিন্ট করা গেল। পুরো ক্রম তৈরির ঠিক আয়না।

initializer list-এ আপনি যে ক্রমেই লিখুন, আসল ক্রম ঠিক করে ঘোষণা: আগে base class (যে ক্রমে :-এর পরে লেখা), তারপর সদস্যরা যে ক্রমে class-এ ঘোষিত। : rate(r), Account(o) লিখলেও Account আগে তৈরি হবে — আর g++ -Wall-এ -Wreorder সতর্কবার্তা দেবে। তাই initializer list সবসময় আসল ক্রমেই লিখুন, যাতে পড়ার সময় ভুল ধারণা না হয়।

একই constructor বারবার লিখবেন না — using Base::Base

কখনো derived class নতুন কোনো data যোগ করে না, শুধু নতুন আচরণ যোগ করে। তখনো base-এর প্রতিটি constructor-এর জন্য একটি করে “যুক্তি নিয়ে base-কে দিয়ে দাও” constructor লেখা বিরক্তিকর। C++11 থেকে একটি লাইনেই সব নিয়ে আসা যায় — একে বলে inheriting constructors:

class Intern : public Employee {
public:
    using Employee::Employee;            // Employee(name, baseSalary) এখন Intern-এরও
    int stipend() const { return std::min(baseSalary(), 15000); }  // ভাতার সর্বোচ্চ সীমা
};

Intern i{"Nusrat", 12000};               // উত্তরাধিকারে পাওয়া constructor

সাবধান: derived class যদি নিজের নতুন data member যোগ করে, inheriting constructor সেটিকে initialize করবে না — শুধু default member initializer (যেমন int level_ = 1;) থাকলে সেটি কাজে লাগবে। নতুন data-র জন্য সঠিক মান দরকার হলে নিজের constructor লিখুন।

৫ · Name hiding — একই নামের ফাংশন base-এর সবকিছু ঢেকে দেয়

এখন একটি ফাঁদ, যা অভিজ্ঞ প্রোগ্রামারদেরও অবাক করে। ধরুন Employee-এর দুটি pay ফাংশন আছে (পর্ব ৯-এর overloading): একটি শুধু টাকার অঙ্ক নেয়, আরেকটি সাথে একটি নোট। Intern-এর জন্য নিয়ম আলাদা — ভাতার সীমা আছে — তাই Intern নিজের pay(int) লিখল:

class Employee {
public:
    void pay(int taka);
    void pay(int taka, const std::string& note);
};
class Intern : public Employee {
public:
    void pay(int taka);                  // সীমা যাচাই করে টাকা দেয়
};

Intern n;
n.pay(8000);                             // ঠিক আছে — Intern::pay(int)
n.pay(8000, "July stipend");             // error! কেন?

দ্বিতীয় লাইনটি compile হয় না, যদিও Employee-এ ঠিক এমন একটি ফাংশন আছে। কারণ C++-এর name lookup নাম খোঁজে আগে, overload বাছাই করে পরে। ধাপে ধাপে দেখুন compiler কী ভাবে:

n.pay(8000, "July stipend");ধাপ ১: “pay” নামটি কোথায়?
খোঁজ শুরু n-এর টাইপ থেকে →  Intern
                                │
                                ▼  (পাওয়া না গেলে ওপরে যাবে)
                             Employee
Intern-এর scopeনাম পাওয়া গেল — খোঁজ থামল
Intern     : pay(int)                 ◀ "pay" পাওয়া গেল, এখানেই থামো
Employee   : pay(int)
             pay(int, const std::string&)   ◀ দেখাই হলো না
overload resolutionপ্রার্থী মাত্র একজন — মেলে না
প্রার্থী:  Intern::pay(int)
যুক্তি  :  (int, const char*)      → ২টি যুক্তি, ফাংশন নেয় ১টি

error: no matching function for call to 'Intern::pay(int, const char [13])'
note: candidate: 'void Intern::pay(int)'
class Intern : public Employee { public: using Employee::pay; void pay(int); };সমাধান: base-এর নামগুলো ডেকে আনা
Intern     : pay(int)                         ◀ Intern-এর নিজের (এটিই জেতে)
             pay(int, const std::string&)  ◀ using দিয়ে আনা

n.pay(8000)                   → Intern::pay(int)
n.pay(8000, "July stipend")   → Employee::pay(int, const std::string&)  ✔
  1. নাম খোঁজা শুরু। compiler প্রথমে কোনো ফাংশন বাছে না; সে খোঁজে “pay” নামটি কোথায় ঘোষিত। শুরু হয় object-এর নিজের টাইপ Intern থেকে, না পেলে base-এ ওঠার কথা।
  2. প্রথম scope-এই পাওয়া গেল — খোঁজ থামল। Intern-এ একটি pay আছে। নিয়ম হলো, যে scope-এ নামটি প্রথম পাওয়া যায়, খোঁজ সেখানেই শেষ — Employee-এর দিকে আর তাকানো হয় না। একেই বলে name hiding: derived-এর একটি নাম base-এর একই নামের সব overload ঢেকে দেয়, তাদের প্যারামিটার যা-ই হোক।
  3. এবার overload বাছাই — কিন্তু প্রার্থী একজন। overload resolution শুধু পাওয়া প্রার্থীদের মধ্যে চলে। Intern::pay নেয় একটি যুক্তি, আমরা দিয়েছি দুটি — মেলে না, তাই error. compiler-এর note-এ শুধু Intern-এর ফাংশনের নাম দেখেই সূত্র পাবেন।
  4. সমাধান — using Employee::pay; এই using-declaration Employee-এর সব pay overload-কে Intern-এর scope-এ নিয়ে আসে। এখন প্রার্থী দুজন (Intern-এর নিজের pay(int) একই স্বাক্ষরের base-ফাংশনকে তখনো ঢেকে রাখে)। দুটি ডাকই ঠিক জায়গায় যায়।

নিয়মটি কেন এমন? ভাবুন, Employee লেখা হয়েছে অন্য দলে, আর পরে কেউ সেখানে নতুন একটি pay(double) যোগ করলেন। name hiding না থাকলে Intern-এর পুরোনো কোড n.pay(8000) হঠাৎ নতুন কোনো overload-এ চলে যেতে পারত, কেউ টের না পেয়ে। C++ তাই নিরাপদ পথ নিয়েছে: derived class নামটি নিজে ঘোষণা করলে সেই নামের পুরো দায়িত্ব তার। base-এর overload চাইলে স্পষ্ট করে using লিখে আনতে হবে।

name hiding একই নামের data member-এর বেলাতেও ঘটে: derived-এ int id; আর base-এও int id; থাকলে derived object-এ দুটি আলাদা id থাকবে, আর d.id শুধু derived-এরটি দেখাবে (base-এরটি d.Employee::id). এমন নাম জেনেশুনে কখনো রাখবেন না।

৬ · Object slicing — কপি করলে derived অংশ কেটে যায়

অংশ ২-এর মেমরির ছবি মনে করুন: SavingsAccount-এর ভেতরে একটি Account অংশ, তার নিচে rate. এখন যদি একটি SavingsAccount-কে একটি Account ভ্যারিয়েবলে কপি করেন, কী হবে? Account ভ্যারিয়েবলে জায়গা আছে শুধু owner আর balance-এর জন্য। rate রাখার জায়গাই নেই। তাই কপি হয় শুধু base অংশ — বাকিটা কেটে বাদ পড়ে, যেভাবে পাউরুটির এক টুকরো কেটে নেওয়া হয়। একে বলে object slicing.

এখানে সমস্যা শুধু একটি সংখ্যা হারানো নয়। হারায় object-এর পরিচয়। দেখতে নিচের উদাহরণে আমরা পর্ব ২৫-এর একটি জিনিস আগেভাগে ব্যবহার করছি — virtual function: virtual লেখা ফাংশন ডাকলে object-এর আসল টাইপের সংস্করণ চলে। এর পুরো ব্যাখ্যা পরের পর্বে; আজ শুধু দেখুন slicing সেই আসল টাইপকে কীভাবে মুছে দেয়:

#include <print>
#include <string>
#include <vector>

struct Account {
    double balance = 0;
    virtual std::string kind() const { return "Account"; }   // virtual: see part 25
    virtual ~Account() = default;
};
struct Savings : Account {
    double rate = 4.0;
    std::string kind() const override { return "Savings"; }
};
void byValue(Account a)      { std::println("by value    : {}", a.kind()); }
void byRef(const Account& a) { std::println("by reference: {}", a.kind()); }
int main() {
    Savings s;
    byValue(s);  byRef(s);
    std::vector<Account> v{s};                  // copied into an Account slot
    std::println("in vector   : {}", v[0].kind());
    std::println("sizes       : {} vs {}", sizeof(Account), sizeof(Savings));
}

আউটপুট: by value : Account, by reference: Savings, in vector : Account, আর 64-bit g++-এ sizes : 16 vs 24. একই s তিনবার পাঠানো হয়েছে, কিন্তু দুবার সে নিজেকে “Account” বলছে। by value পাঠানোয় ফাংশনের a একটি নতুন Account object, যা s-এর শুধু Account অংশ কপি করে বানানো — সে আর Savings নয়। reference-এ কোনো কপি হয়নি, a আসল s-কেই দেখছে, তাই “Savings”. আর std::vector<Account>-এর প্রতিটি ঘর ঠিক sizeof(Account) বাইটের — Savings-এর বাড়তি ৮ বাইট রাখার জায়গাই নেই। (আকার platform-ভেদে আলাদা হতে পারে; Account-এর ১৬ বাইটের মধ্যে একটি লুকোনো pointer আছে, যা virtual-এর কাজে লাগে — পর্ব ২৫-এ দেখবেন।)

Savings s;পূর্ণ Savings object
s :  ┌───────────────────────────┐
     │ [টাইপ-চিহ্ন → Savings]     │
     │ balance = 0               │  ◀ Account অংশ
     ├───────────────────────────┤
     │ rate    = 4.0             │  ◀ Savings অংশ
     └───────────────────────────┘
byValue(s); // void byValue(Account a)নতুন Account বানানো হলো — শুধু base অংশ কপি
s :  ┌───────────────────────────┐        a :  ┌───────────────────────────┐
     │ [→ Savings]               │             │ [টাইপ-চিহ্ন → Account]    │
     │ balance = 0               │ ──কপি──▶    │ balance = 0               │
     ├───────────────────────────┤             └───────────────────────────┘
     │ rate    = 4.0             │ ──✂──  কোথাও যায় না
     └───────────────────────────┘
a.kind()a তো সত্যিই একটি Account
a.kind()  →  "Account"

a-কে Account-এর copy constructor বানিয়েছে, তাই a জন্ম থেকেই Account.
এটা bug নয় — ভাষা যা বলেছে, তা-ই করেছে। ভুল আমাদের: by value চেয়েছি।
byRef(s); // void byRef(const Account& a)কোনো কপি নেই — a মানে s নিজেই
s :  ┌───────────────────────────┐
a ──▶│ [→ Savings]               │
     │ balance = 0               │
     ├───────────────────────────┤
     │ rate    = 4.0             │   ◀ অক্ষত
     └───────────────────────────┘
a.kind()  →  "Savings"
std::vector<Account> v{s};প্রতিটি ঘর ঠিক Account-এর মাপের
v :  [ Account | Account | Account | ... ]   প্রতি ঘর sizeof(Account)
           ▲
           └── s-এর শুধু Account অংশ, rate কেটে গেছে

সমাধান: std::vector<std::unique_ptr<Account>> v;
         v.push_back(std::make_unique<Savings>());   // object আস্ত থাকে heap-এ
  1. পূর্ণ object. s-এর ভেতরে Account অংশ, তার নিচে rate. সাথে একটি লুকোনো “টাইপ-চিহ্ন” (virtual থাকলে compiler রাখে), যা বলে এটি আসলে Savings.
  2. By value মানে নতুন object. byValue-এর প্যারামিটার Account a — একটি সম্পূর্ণ নতুন Account. সেটি বানানো হয় Account-এর copy constructor দিয়ে, যে শুধু Account-এর সদস্যদের চেনে। rate-এর জন্য a-তে জায়গাই নেই, তাই কেটে বাদ।
  3. পরিচয় হারাল। a-এর টাইপ-চিহ্ন Account-এর, কারণ a আসলেই Account. virtual ফাংশন ডাকলেও Account-এর সংস্করণ চলে। লক্ষ করুন: compiler কোনো error বা সতর্কবার্তা দেয়নি — slicing নিঃশব্দ।
  4. Reference — কপি নেই, কাটাও নেই। const Account& নতুন object বানায় না; s-এর base অংশের দিকে তাকায়, আর পুরো s অক্ষত থাকে। তাই আসল টাইপের আচরণ পাওয়া যায়। এ কারণেই পর্ব ২৫-এর polymorphism সবসময় reference বা pointer দিয়ে কাজ করে।
  5. Container-এ slicing. std::vector<Account> value রাখে, আর প্রতিটি ঘর একই মাপের। যেকোনো derived object ঢোকালে কাটা পড়ে। নানা ধরনের হিসাব এক তালিকায় রাখতে হলে std::vector-এ std::unique_ptr<Account> রাখুন (পর্ব ১৭) — object আস্ত থাকে heap-এ, vector শুধু মালিকানা ধরে।
মূল নিয়ম

hierarchy-র object ফাংশনে পাঠাবেন const Base& বা Base& দিয়ে, কখনো Base by value নয়। range-for-এ for (auto e : list) নয়, for (const auto& e : list). আর একসাথে নানা derived টাইপ রাখতে std::vector<std::unique_ptr<Base>>.

৭ · public, protected আর private inheritance

এ পর্যন্ত সব উদাহরণে কোলনের পরে public লিখেছি। আরও দুটি বিকল্প আছে। এগুলো ঠিক করে, base-এর public আর protected সদস্যরা derived-এ এসে কী হবে — আর তার চেয়েও গুরুত্বপূর্ণ, বাইরের দুনিয়া কি জানবে যে “derived এক ধরনের base”?

লেখাbase-এর public হয়ে যায়base-এর protected হয়ে যায়বাইরের কোড কি Derived& → Base& করতে পারে?মানে
: public Basepublicprotectedহ্যাঁis-a — প্রায় সব ক্ষেত্রে এটিই
: protected Baseprotectedprotectedনাখুবই বিরল
: private Baseprivateprivateনা“implemented in terms of” — ভেতরের ব্যাপার

base-এর private সদস্য তিন ক্ষেত্রেই derived-এর কাছে অপ্রবেশ্য থাকে — inheritance-এর ধরন শুধু public আর protected সদস্যদের ভাগ্য বদলায়।

private inheritance-এর অর্থ is-a নয়। এর অর্থ “আমি ভেতরে ভেতরে ওই class দিয়ে বানানো, কিন্তু বাইরে কাউকে বলব না”। বাইরের কোড Derived-কে Base হিসেবে ব্যবহার করতে পারে না। একই কাজ প্রায় সবসময় composition দিয়ে আরও পরিষ্কারভাবে হয় — Base-কে একটি private সদস্য করে রাখা। private inheritance-এর বৈধ ব্যবহার অল্প কিছু, যেমন base-এর কোনো virtual ফাংশন নতুন করে লিখতে হলে, বা খালি class-এর জায়গা বাঁচানোর পুরোনো কৌশলে (C++20 থেকে এর জন্য [[no_unique_address]] সদস্যও আছে)। protected inheritance বাস্তব কোডে আরও বিরল।

পরীক্ষার প্রিয় ফাঁদ: class D : B { }; — কিছু না লিখলে class-এ inheritance ডিফল্টভাবে private, আর struct D : B { };-এ ডিফল্ট public. তাই class দিয়ে লিখলে public শব্দটি ভুলবেন না; ভুললে বাইরে থেকে D-কে B&-এ পাঠাতে গিয়ে অবাক করা error পাবেন (“'B' is an inaccessible base of 'D'”).

পরীক্ষা ও ইন্টারভিউতে
  • derived object তৈরি আর ধ্বংসের ক্রম কী? — তৈরি: base আগে, তারপর derived-এর সদস্যরা ঘোষণার ক্রমে, তারপর derived-এর body; ধ্বংস ঠিক উল্টো ক্রমে।
  • protected আর private-এর পার্থক্য? — দুটিই বাইরের কোড থেকে লুকোনো; protected সদস্য derived class-এর কোড ছুঁতে পারে, private পারে না।
  • class D : B-তে inheritance কোন ধরনের? — private (struct হলে public).
  • object slicing কী? — derived object-কে by value base টাইপে কপি করলে শুধু base অংশ কপি হয়, derived-এর সদস্য আর আসল টাইপের আচরণ হারায়; সমাধান reference বা pointer.
  • derived-এ একই নামের ফাংশন লিখলে base-এর overload কেন পাওয়া যায় না? — name hiding: নাম প্রথম যে scope-এ পাওয়া যায়, খোঁজ সেখানেই থামে; using Base::f; দিয়ে ফেরত আনা যায়।

৮ · Multiple inheritance, diamond আর final

C++-এ একটি class একাধিক base থেকে inherit করতে পারে — একে বলে multiple inheritance. যেমন একটি স্মার্টফোন এক ধরনের ক্যামেরা, আবার এক ধরনের ফোনও: class SmartPhone : public Camera, public Phone. বাস্তবে সবচেয়ে নিরাপদ আর প্রচলিত ব্যবহার হলো একটি আসল base আর কয়েকটি ছোট “interface” (যাদের data নেই, শুধু ফাংশনের প্রতিশ্রুতি — পর্ব ২৬-এ)। দুটি base-এ একই নামের সদস্য থাকলে phone.Camera::powerOn() লিখে স্পষ্ট করতে হয়, নইলে compiler “ambiguous” বলে থামে।

Diamond সমস্যা

ঝামেলা বাধে যখন দুটি base নিজেরাই একই base থেকে আসে। ধরুন একটি বিশ্ববিদ্যালয়ের সিস্টেমে Person থেকে Student আর Staff, আর একজন টিচিং অ্যাসিস্ট্যান্ট দুটিই — TA : Student, Staff. ছবিটা হীরার (diamond) মতো:

//          Person
//         /      \    (দুই দিকে)
//    Student    Staff
//         \      /
//            TA
struct Person  { std::string name; };
struct Student : Person { int roll; };
struct Staff   : Person { int salary; };
struct TA : Student, Staff { };

TA t;
t.name = "Mim";       // error: request for member 'name' is ambiguous
                      // TA-র ভেতরে দুটি Person অংশ — কোনটির name?

সাধারণ inheritance-এ প্রতিটি base অংশ আলাদা, তাই TA-র ভেতরে দুটি আলাদা Person — দুটি নাম, যা একসময় আলাদা হয়ে যেতে পারে। এটাই diamond problem. C++-এর উত্তর virtual inheritance: struct Student : virtual Person আর struct Staff : virtual Person লিখলে TA-র ভেতরে Person অংশ থাকে একটিই, দুজনে ভাগ করে নেয়। standard library-তেও এর উদাহরণ আছে: std::iostream আসে std::istream আর std::ostream দুটি থেকে, আর তারা দুজনেই virtual-ভাবে একই base (std::basic_ios) থেকে।

তবে virtual inheritance-এর দাম আছে। শেয়ার করা base অংশটির constructor ডাকার দায়িত্ব চলে যায় সবচেয়ে নিচের class-এর (এখানে TA) কাছে, মাঝের Student বা Staff-এর initializer list তখন সেই ডাক উপেক্ষা হয়। object-এর বিন্যাস জটিল হয়, base অংশে পৌঁছাতে বাড়তি ধাপ লাগে। তাই নতুন নকশায় diamond দেখলে আগে প্রশ্ন করুন, নকশাটাই ঠিক কি না। প্রায়ই উত্তর হয়: TA একজন Person, যার একটি Student-ভূমিকা আর একটি Staff-ভূমিকা আছে — composition.

final — এখানেই শেষ

কিছু class-কে আর derive করা উচিত নয় — হয়তো তার invariant এত সূক্ষ্ম যে কেউ বাড়িয়ে লিখলে ভাঙবে, বা আপনি চান ভবিষ্যতে ভেতরের নকশা স্বাধীনভাবে বদলাতে। C++11 থেকে final দিয়ে সেটা বলা যায়:

class Money final { /* ... */ };
class MyMoney : public Money { };   // error: cannot derive from 'final' base 'Money'

final virtual ফাংশনের ওপরও বসে (“এর পরে কেউ এই ফাংশন বদলাতে পারবে না”) — সেটি পর্ব ২৫-এ। একটি ভালো অভ্যাস: যে class-কে আপনি base হিসেবে ব্যবহারের জন্য নকশা করেননি, তাকে খোলা রাখার বদলে final ভাবুন।

Bjarne Stroustrup-এর প্রথম “C with Classes” (১৯৭৯ সাল থেকে) ভাষাতেই derived class ছিল — ধারণাটি তিনি নিয়েছিলেন Simula 67 থেকে, যেটি নরওয়েতে Ole-Johan Dahl আর Kristen Nygaard নকশা করেছিলেন এবং যাকে প্রথম object-oriented ভাষা ধরা হয়। multiple inheritance যোগ হয় পরে, ১৯৮৯ সালের C++ 2.0-তে, virtual base class সহ। final, override আর inheriting constructors এসেছে আরও অনেক পরে, C++11-এ।

৯ · Liskov নীতি — বর্গ কি সত্যিই এক ধরনের আয়ত?

is-a কথাটা মুখে বলা সহজ। কিন্তু কীভাবে যাচাই করবেন, সম্পর্কটা সত্যিই is-a কি না? এর পরিষ্কার উত্তর দিয়েছেন কম্পিউটার বিজ্ঞানী Barbara Liskov. সহজ ভাষায় তাঁর নীতি — Liskov Substitution Principle (LSP): যে কোড base টাইপের object নিয়ে ঠিকঠাক চলে, তাকে একটি derived object দিলেও সে ঠিকঠাক চলবে — কোনো চমক ছাড়া। derived class base-এর প্রতিশ্রুতি রাখবে, ভাঙবে না।

বিখ্যাত উদাহরণটি দেখুন। গণিতে প্রতিটি বর্গ একটি আয়ত — চার কোণ সমকোণ, শুধু বাহু চারটি সমান। তাহলে কোডে Square : public Rectangle তো স্বাভাবিক, তাই না?

class Rectangle {
public:
    virtual void setWidth(int w)  { w_ = w; }
    virtual void setHeight(int h) { h_ = h; }
    int area() const { return w_ * h_; }
private:
    int w_ = 0, h_ = 0;
};
class Square : public Rectangle {          // “বর্গ এক ধরনের আয়ত”?
public:
    void setWidth(int w)  override { Rectangle::setWidth(w); Rectangle::setHeight(w); }
    void setHeight(int h) override { Rectangle::setWidth(h); Rectangle::setHeight(h); }
};

void resize(Rectangle& r) {                // Rectangle-এর জন্য লেখা, যুক্তিসংগত কোড
    r.setWidth(5);
    r.setHeight(4);
    assert(r.area() == 20);                // Square দিলে area 16 — ভেঙে গেল!
}

resize-এর লেখক একটি যুক্তিসংগত ধারণা করেছেন: আয়তের প্রস্থ আর উচ্চতা আলাদাভাবে বদলানো যায়। Rectangle-এর প্রতিশ্রুতি এটাই। Square সেই প্রতিশ্রুতি রাখতে পারে না — উচ্চতা বদলালে প্রস্থও বদলে যায়। ফলে Square-কে Rectangle-এর জায়গায় বসালে কোড ভাঙে। গণিতের “is-a” আর কোডের “is-a” এক নয়। কোডে is-a মানে আচরণে বদলে ব্যবহারযোগ্য।

সমাধান কী? কয়েকটি পথ আছে, পরিস্থিতি বুঝে: (ক) আকারগুলোকে বদলানোর অযোগ্য (immutable) রাখুন — নতুন মাপ চাইলে নতুন object. তখন “প্রস্থ আলাদা বদলানো” প্রতিশ্রুতিটাই থাকে না, আর Square-কে Rectangle-এর মতো ব্যবহার করা নিরাপদ। (খ) Square আর Rectangle দুজনকেই একটি সাধারণ Shape-এর সন্তান করুন, যার প্রতিশ্রুতি শুধু “আমার একটি ক্ষেত্রফল আছে” — কেউ কারও সন্তান নয়। (গ) আদৌ hierarchy না রেখে আলাদা টাইপ রাখুন।

inheritance লেখার আগে প্রশ্ন“না” হলে
বাক্যে বলতে পারি “প্রতিটি D এক ধরনের B”, আর ডোমেইনের মানুষ তাতে একমত?composition বা আলাদা টাইপ
B& নেয় এমন সব ফাংশনকে D দিলে তারা ঠিক চলবে? (Liskov)hierarchy বদলান — যেমন Square/Rectangle
D কি B-এর কোনো ফাংশনকে “এখানে কাজ করে না” বলে exception ছোড়ায় বা খালি রাখে?প্রতিশ্রুতি ভাঙছে — নকশা ভুল
আমি কি শুধু B-এর কিছু কোড ধার নিতে চাই?composition (পর্ব ২৭)
hierarchy কি তিন স্তরের বেশি গভীর হচ্ছে?থামুন, সরল করুন
inheritance-এর প্রশ্ন “কোড কোথা থেকে ধার নেব?” নয়।
প্রশ্ন হলো — “কে কার জায়গায় নিশ্চিন্তে বসতে পারে?”

১০ · AI-এর যুগে — AI-এর বানানো class hierarchy যাচাই

“একটি HR সিস্টেম লেখো” বললে AI টুল প্রায়ই চমৎকার দেখতে একটি class hierarchy দেয় — BaseEntity থেকে Person, তার থেকে Employee, তার থেকে FullTimeEmployee, তার থেকে SeniorManager… কারণ প্রশিক্ষণের অনেক পুরোনো Java-ধাঁচের কোডে এমন গভীর গাছ আছে। কোড compile হয়, চলেও। সমস্যা ধরা পড়ে পরে — নতুন একটি নিয়ম যোগ করতে গিয়ে, বা যখন দেখা যায় বেতনের হিসাব কিছু কর্মচারীর জন্য ভুল আসছে। AI-এর সাধারণ ভুলগুলো:

AI যা করেসমস্যাযা হওয়া উচিত
শুধু কোড ভাগ করতে গভীর hierarchy (৪–৫ স্তর)একটি base বদলালে নিচের সব ভাঙে; কোথায় কোন আচরণ, খুঁজে পাওয়া কঠিনঅগভীর hierarchy; কোড ভাগে composition বা free function
সব data protectedinvariant প্রতিটি derived-এর হাতে; base নিজের নিয়ম রক্ষা করতে পারে নাdata private, দরকারে protected ফাংশন
std::vector<Employee>-এ Manager, Intern ঢোকানোনিঃশব্দ slicing — সবাই সাধারণ Employee হয়ে যায়std::vector<std::unique_ptr<Employee>>
void printPayslip(Employee e), for (auto e : staff)প্রতি ডাকে কপি আর slicingconst Employee&, const auto&
derived-এ একই নামের ফাংশন, অথচ base-এ overload আছেname hiding — অন্য overload হঠাৎ “নেই”using Base::f; বা আলাদা নাম
class Manager : Employee (public ভুলে যাওয়া)private inheritance — Manager-কে Employee&-এ পাঠানো যায় না: public Employee
Square : Rectangle, Stack : std::vectorLiskov ভাঙে, বা ভুল প্রতিশ্রুতি বাইরে যায়আলাদা টাইপ বা composition

AI-কে hierarchy লিখতে বলার আগে নকশার নিয়ম দিয়ে দিন, আর প্রতিটি inheritance-এর পক্ষে যুক্তি চান:

আমি C++23 শিখছি (g++, -Wall -Wextra). একটি ছোট অফিসের বেতন-ব্যবস্থার মডেল লেখো: Employee, Manager (মূল বেতন + দলের প্রতি সদস্যের জন্য ভাতা) আর Intern (ভাতার একটি সর্বোচ্চ সীমা আছে)। নিয়ম: ১. hierarchy সর্বোচ্চ দুই স্তর। প্রতিটি inheritance-এর পাশে এক লাইনে লেখো কেন এটা সত্যিকারের is-a, আর Liskov নীতি কীভাবে রক্ষা পাচ্ছে। ২. সব data member private; derived-দের কিছু দরকার হলে protected ফাংশন দাও, protected data নয়। ৩. base-এর constructor initializer list থেকে ডাকো; নতুন data না থাকলে using Base::Base ব্যবহার করো। ৪. কোথাও Employee by value পাঠাবে না; সবাইকে এক তালিকায় রাখতে std::vector<std::unique_ptr<Employee>>. ৫. যে সম্পর্ক has-a (যেমন Employee-এর একটি Address বা Department), সেখানে inheritance নয়, সদস্য রাখো। ৬. প্রতিটি constructor আর destructor-এ একটি লাইন প্রিন্ট করো, যাতে আমি তৈরি ও ধ্বংসের ক্রম মিলিয়ে দেখতে পারি। কোডের পরে একটি তালিকা দাও: কোন জায়গায় slicing বা name hiding হতে পারত, আর তুমি কীভাবে এড়িয়েছ।

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

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

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

প্র ০১Barbara Liskov ১৯৮৭ সালে কী বলেছিলেন, কেন সেটি পরে “Liskov Substitution Principle” নামে বিখ্যাত হলো, আর এটি আসলে টাইপের নয়, আচরণের নিয়ম কেন?

Barbara Liskov ১৯৮৭ সালে OOPSLA সম্মেলনে “Data Abstraction and Hierarchy” নামে একটি keynote বক্তৃতা দেন। সেখানে তিনি প্রশ্ন তোলেন: একটি টাইপকে আরেকটির subtype বলার আসল শর্ত কী? তাঁর উত্তরের সারকথা — যদি টাইপ S-এর object-কে টাইপ T-এর object-এর জায়গায় বসালে T-এর জন্য লেখা কোনো প্রোগ্রামের আচরণ বদলে না যায়, তবেই S হলো T-এর subtype. পরে ১৯৯৪ সালে Jeannette Wing-এর সাথে লেখা একটি গবেষণাপত্রে (“A Behavioral Notion of Subtyping”) তাঁরা ধারণাটিকে আনুষ্ঠানিক রূপ দেন: subtype-কে base-এর precondition দুর্বল বা সমান রাখতে হবে (বেশি দাবি করা যাবে না), postcondition শক্ত বা সমান রাখতে হবে (কম প্রতিশ্রুতি দেওয়া যাবে না), আর base-এর invariant রক্ষা করতে হবে।

“LSP” নামটি জনপ্রিয় করেন Robert C. Martin. ১৯৯০-এর দশকে তিনি C++ নিয়ে লেখা প্রবন্ধে এটিকে object-oriented নকশার কয়েকটি মূল নীতির একটি হিসেবে তুলে ধরেন, আর পরে সেগুলো একসাথে “SOLID” নামে পরিচিত হয় — L অক্ষরটি Liskov-এর। Square/Rectangle উদাহরণটিও তাঁর লেখার মাধ্যমে ব্যাপক পরিচিতি পায়। Liskov নিজে ২০০৮ সালে Turing Award পান — data abstraction, fault tolerance আর distributed computing-এ তাঁর অবদানের জন্য।

কেন এটি আচরণের নিয়ম? compiler শুধু টাইপ যাচাই করে: Square-এ setWidth আছে কি না, তার স্বাক্ষর মেলে কি না। Square/Rectangle উদাহরণে সব মেলে — compile হয় নিখুঁতভাবে। ভাঙে শুধু একটি অলিখিত প্রতিশ্রুতি: “প্রস্থ বদলালে উচ্চতা একই থাকে”। এই প্রতিশ্রুতি কোনো C++ syntax-এ লেখা নেই; থাকে ডকুমেন্টেশনে, টেস্টে, আর ব্যবহারকারীর মাথায়। তাই LSP মানতে হলে প্রোগ্রামারকে ভাবতে হয় base class-এর চুক্তি নিয়ে — শুধু তার ফাংশনের নাম নিয়ে নয়। এ কারণেই ভালো base class তার প্রতিটি ফাংশনের প্রতিশ্রুতি স্পষ্ট লিখে রাখে, আর ভালো টেস্ট-সেট base-এর জন্য লেখা টেস্ট প্রতিটি derived-এর ওপরও চালায়।

LSP-এর একটি ব্যবহারিক লক্ষণ মনে রাখুন: derived class যদি base-এর কোনো ফাংশনকে “আমার ক্ষেত্রে প্রযোজ্য নয়” বলে exception ছোড়ায় বা খালি রেখে দেয় — যেমন Penguin : Bird-এ fly() — তাহলে প্রায় নিশ্চিতভাবে নীতিটি ভাঙছে।

মূল উপলব্ধি: inheritance-এর সঠিকতা compiler প্রমাণ করতে পারে না; সে শুধু syntax মেলায়। “D কি B-এর জায়গায় নিশ্চিন্তে বসতে পারে?” — এই আচরণগত প্রশ্নের উত্তর দেওয়ার দায়িত্ব নকশাকারীর।

প্র ০২১৯৯৪ সালের বিখ্যাত “Gang of Four” বই কেন বলেছিল “class inheritance-এর চেয়ে object composition বেছে নিন”? inheritance কি তাহলে খারাপ?

১৯৯৪ সালে Erich Gamma, Richard Helm, Ralph Johnson আর John Vlissides “Design Patterns: Elements of Reusable Object-Oriented Software” বইটি প্রকাশ করেন; চার লেখকের জন্য একে “Gang of Four” (GoF) বলা হয়। বইটির উদাহরণ মূলত C++ আর Smalltalk-এ। শুরুর অধ্যায়েই তাঁরা দুটি নীতি দেন: “একটি implementation নয়, interface-এর দিকে প্রোগ্রাম করুন” আর “class inheritance-এর চেয়ে object composition বেছে নিন”।

তাঁদের যুক্তি ছিল পুনর্ব্যবহারের দুই ধরন নিয়ে। inheritance-কে তাঁরা বলেন “white-box reuse”: derived class base-এর ভেতরটা দেখে, তার protected অংশ ছোঁয়, তার ফাংশন কীভাবে একে অপরকে ডাকে তার ওপর নির্ভর করে। ফলে base-এর implementation বদলালে derived ভাঙতে পারে। বইয়ের ভাষায়, যেহেতু inheritance subclass-কে parent-এর implementation-এর খুঁটিনাটির সামনে খুলে দেয়, তাই প্রায়ই বলা হয় “inheritance encapsulation ভাঙে”। তার ওপর inheritance-এর সম্পর্ক compile-এর সময় স্থির — চলার সময় একটি object তার base বদলাতে পারে না। composition হলো “black-box reuse”: আপনি অন্য object-কে শুধু তার public interface দিয়ে ব্যবহার করেন, ভেতর জানেন না, আর চাইলে চলার সময় অন্য object বসিয়ে দিতে পারেন। বইয়ের অনেক pattern — Strategy, Decorator, Composite, Bridge — আসলে এই নীতির প্রয়োগ।

তাহলে কি inheritance খারাপ? না। শব্দটা “favor” — বেছে নেওয়ার ঝোঁক, নিষেধ নয়। GoF নিজেরাই বলেছেন, composition-এর জন্য ভালো interface লাগে, আর interface-এর hierarchy বানাতে inheritance লাগে। সমস্যা হয় যখন inheritance-কে প্রথম হাতিয়ার ধরা হয় — কোড ভাগ করার শর্টকাট হিসেবে। নব্বইয়ের দশকের অনেক বড় GUI আর ব্যবসায়িক framework-এ ছয়-সাত স্তরের hierarchy তৈরি হয়েছিল, যেখানে একটি base বদলানো মানে শত শত class-এর আচরণ অনিশ্চিত হওয়া। আধুনিক C++ সম্প্রদায় এই অভিজ্ঞতা থেকে শিখেছে: অগভীর hierarchy, interface-ধাঁচের base, আর বাকি সব composition, template বা std::variant দিয়ে।

বাস্তব নিয়ম হিসেবে: is-a সত্যি হলে, আর আপনি চান base টাইপের জায়গায় derived ব্যবহৃত হোক — inheritance. শুধু “ওই class-এর কয়েকটা ফাংশন কাজে লাগবে” — composition. পর্ব ২৭-এ এই সিদ্ধান্তটি SOLID নীতির সাথে মিলিয়ে বিস্তারিত দেখবেন।

মূল উপলব্ধি: inheritance দুটি জিনিসকে শক্ত করে বেঁধে দেয় — টাইপের সম্পর্ক আর implementation-এর নির্ভরতা। শুধু প্রথমটি দরকার হলে দ্বিতীয়টির দাম দেওয়ার কারণ নেই।

প্র ০৩C++ class-এর multiple inheritance দেয়, কিন্তু Java ইচ্ছা করে দেয়নি। দুই ভাষার নকশাকারীরা কী ভেবেছিলেন, আর পরে কী হলো?

C++-এ multiple inheritance যোগ হয় ১৯৮৯ সালের C++ 2.0-তে। Stroustrup-এর যুক্তি ছিল, বাস্তব জগতে একটি জিনিস একাধিক শ্রেণির অংশ হতে পারে, আর library-লেখকদের জন্য একাধিক স্বাধীন ক্ষমতা জোড়া লাগানোর উপায় দরকার। C++-এর দর্শনও এখানে কাজ করেছে: ভাষা ক্ষমতা দেবে, আর যিনি ব্যবহার করবেন না, তাঁকে তার দাম দিতে হবে না। সাধারণ single inheritance-এর কোডে multiple inheritance থাকার জন্য কোনো বাড়তি খরচ নেই। তবে এর জটিলতাও কম নয়: দুই base-এ একই নামের ফাংশন হলে ambiguity, diamond-এ base-এর দুটি কপি, virtual inheritance-এর বাড়তি নিয়ম, আর কোন constructor কে ডাকবে তার সূক্ষ্ম নিয়ম। তখন থেকেই বিতর্ক চলছে, এটি কি দাম অনুযায়ী লাভ দেয়।

Java (প্রথম প্রকাশ ১৯৯৫) তৈরির সময় James Gosling-এর দল C++-এর অভিজ্ঞতা দেখেছিলেন, আর ইচ্ছা করে ভাষাকে সরল রাখতে চেয়েছিলেন। তাঁরা একটি মাঝামাঝি পথ নেন: একটি class-এর base class থাকবে একটিই (extends), কিন্তু যত খুশি interface (implements). interface-এ তখন শুধু ফাংশনের ঘোষণা থাকত, কোনো data বা implementation নয়। ফলে diamond-এর আসল সমস্যা — একই data-র দুটি কপি, একই ফাংশনের দুটি ভিন্ন implementation — জন্মই নিত না। C# প্রায় একই পথ অনুসরণ করে।

মজার ব্যাপার হলো, সময়ের সাথে Java আংশিক পিছিয়ে এসেছে। ২০১৪ সালের Java 8-এ interface-এ “default method” — অর্থাৎ implementation সহ ফাংশন — যোগ হয়, কারণ পুরোনো interface-এ নতুন ফাংশন যোগ করলে সব implementing class ভেঙে যেত। এর সাথে ফিরে আসে ছোট আকারের diamond প্রশ্ন: দুই interface-এ একই default method থাকলে কোনটি? Java তখন নিয়ম দেয়, class-কে স্পষ্ট করে বেছে নিতে হবে — ঠিক C++-এর মতো। আবার Python পূর্ণ multiple inheritance দেয়, আর কোন base-এর ফাংশন আগে খোঁজা হবে, তা একটি নির্দিষ্ট অ্যালগরিদম (C3 linearization) দিয়ে ঠিক করে।

আধুনিক C++ চর্চায় সবচেয়ে প্রচলিত ধরন আসলে Java-র কাছাকাছি: একটি “আসল” base (বা কোনোটিই নয়) আর কয়েকটি data-বিহীন abstract interface. পর্ব ২৬-এ এই ধরনটাই দেখবেন। data সহ দুই বড় base জোড়া লাগানো, বা diamond — বিরল, আর সাধারণত নকশা নতুন করে ভাবার সংকেত।

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

প্র ০৪“Fragile base class problem” কী? base class-এ নিরীহ একটি বদল কীভাবে এমন derived class ভেঙে দিতে পারে, যার কোড কেউ ছোঁয়নি?

fragile base class সমস্যা মানে: base class-এ এমন একটি বদল, যা base-এর নিজের দৃষ্টিতে সম্পূর্ণ নিরাপদ, তবু derived class-কে ভেঙে দেয়। “fragile” কারণ base-কে তখন আর স্বাধীনভাবে বদলানো যায় না — সব derived class-এর গোপন নির্ভরতার কথা ভাবতে হয়, যাদের অনেকগুলো হয়তো অন্য দলের বা অন্য কোম্পানির লেখা।

এর দুটি রূপ আছে। প্রথমটি আচরণগত। Joshua Bloch তাঁর “Effective Java” বইয়ে একটি বিখ্যাত উদাহরণ দেন: কেউ Java-র HashSet থেকে derive করে একটি class লিখলেন, যা গোনে কতগুলো উপাদান যোগ হয়েছে — add আর addAll দুটিই override করে গোনা বাড়ান। ফল ভুল আসে: addAll দিয়ে তিনটি উপাদান যোগ করলে গোনা হয় ছয়। কারণ base-এর addAll ভেতরে ভেতরে add ডাকে, তাই প্রতিটি উপাদান দুবার গোনা হয়। এই “ভেতরে add ডাকে” তথ্যটি base-এর implementation-এর খুঁটিনাটি, কোনো প্রতিশ্রুতি নয় — library-র পরের সংস্করণে বদলাতে পারে, আর তখন derived class আবার অন্যভাবে ভাঙবে। এটাই GoF-এর “inheritance encapsulation ভাঙে” কথার বাস্তব রূপ। protected data থাকলে বিপদ আরও বাড়ে, কারণ derived class base-এর ভেতরের অবস্থার ওপর সরাসরি নির্ভর করে।

দ্বিতীয় রূপটি C++-এ বিশেষ করে তীব্র — binary বা ABI স্তরের। অংশ ২-এ দেখেছেন, derived object-এর ভেতরে base অংশ বসানো থাকে, আর derived-এর সদস্যরা তার পরে নির্দিষ্ট offset-এ। এখন base-এ একটি নতুন private data member যোগ করলেন — source কোডে কারও কিছু বদলায়নি। কিন্তু base-এর আকার বদলেছে, তাই প্রতিটি derived class-এর সদস্যদের offset সরে গেছে। আগে compile করা কোনো library বা plugin, যা পুরোনো বিন্যাস ধরে নিয়ে বানানো, এখন ভুল ঠিকানায় পড়বে-লিখবে। ফলে সব derived কোড আবার compile করতেই হয়। বড় C++ library এই কারণে নানা কৌশল নেয়: Qt-এর মতো framework public class-এ শুধু একটি pointer রাখে, আর আসল data লুকোনো থাকে আলাদা একটি private class-এ (“d-pointer” বা pimpl কৌশল), যাতে data বদলালেও public class-এর আকার একই থাকে।

প্রতিরোধের উপায়: base-এর data private রাখা, derived-দের জন্য খোলা জায়গাগুলো সংখ্যায় কম আর স্পষ্টভাবে নথিভুক্ত রাখা, base-কে inheritance-এর জন্য নকশা না করলে final করা, আর কোড ভাগাভাগিতে composition বেছে নেওয়া — composition-এ আপনি শুধু public interface-এর ওপর নির্ভর করেন।

মূল উপলব্ধি: inheritance শুধু derived-কে base-এর ওপর নির্ভরশীল করে না, base-কেও derived-দের বন্দি করে ফেলে। প্রতিটি protected সদস্য আর প্রতিটি overridable ফাংশন একটি স্থায়ী প্রতিশ্রুতি, যা পরে ফিরিয়ে নেওয়া কঠিন।

প্র ০৫std::vector<Base> কেন derived object-কে কেটে ফেলে? C++ কেন Java-র মতো সবকিছুকে নিজে থেকে reference বানিয়ে দেয় না, আর বিকল্পগুলো কী?

উত্তরটা C++-এর value semantics-এ। C++-এ একটি ভ্যারিয়েবল মানে একটি object নিজে, তার দিকে তাকানো কোনো হাতল নয়। Account a; লিখলে stack-এ ঠিক sizeof(Account) বাইট জায়গা নেওয়া হয়। std::vector<Account>-ও তেমনি একটানা মেমরিতে পাশাপাশি Account object রাখে, প্রতিটি ঠিক একই মাপের — এটাই vector-কে দ্রুত করে (পর্ব ১৩-এর cache-এর আলোচনা মনে করুন)। একটি Savings সেখানে রাখতে চাইলে compiler-এর হাতে একটাই উপায়: Savings-এর Account অংশ কপি করে একটি Account বানানো। বাড়তি সদস্য রাখার জায়গা নেই, আর ঘরের মাপ চলার সময় বদলানো যায় না, কারণ তাহলে v[i]-এর ঠিকানা আর সরল গুণ দিয়ে বের করা যেত না।

Java, C# বা Python অন্য পথ নিয়েছে: class-এর প্রতিটি object থাকে heap-এ, আর ভ্যারিয়েবল আসলে একটি reference. List<Account> রাখে শুধু reference-এর তালিকা, তাই যেকোনো subclass ঢুকতে পারে, slicing ঘটেই না। দাম: প্রতিটি object-এর জন্য আলাদা heap বরাদ্দ, garbage collector, আর তালিকার object-গুলো মেমরিতে ছড়ানো — পর পর পড়তে গেলে cache-এ ঘন ঘন miss. C++ এই দাম সবার ওপর চাপাতে চায়নি। যে টাইপের polymorphism দরকার নেই (int, string, Point, Date) — যা বেশিরভাগ টাইপ — তারা value হিসেবে দ্রুত আর সরল থাকে।

তাহলে নানা derived টাইপ এক তালিকায় রাখতে কী করবেন? (ক) std::vector<std::unique_ptr<Base>> — সবচেয়ে প্রচলিত; প্রতিটি object heap-এ আস্ত থাকে, মালিকানা পরিষ্কার (পর্ব ১৭)। (খ) টাইপের তালিকা আগে থেকে জানা আর ছোট হলে std::variant<Savings, Current, Loan> (পর্ব ৩৮) — inheritance ছাড়াই value হিসেবে, heap ছাড়া। (গ) object অন্য কোথাও থাকলে শুধু দেখার জন্য std::vector<Base*> বা std::reference_wrapper. Sean Parent ২০১৩ সালের একটি বিখ্যাত বক্তৃতায় (“Inheritance Is The Base Class of Evil”) দেখিয়েছিলেন আরেকটি কৌশল — type erasure, যেখানে বাইরে value semantics থাকে আর polymorphism লুকোনো থাকে ভেতরে; std::function এভাবেই কাজ করে।

অনেক অভিজ্ঞ প্রোগ্রামার polymorphic base class-এর copy constructor বন্ধ (delete বা protected) করে রাখেন, যাতে by value কপি — অর্থাৎ slicing — compile-ই না হয়। C++ Core Guidelines-ও polymorphic class-এর public copy বন্ধ রাখার পরামর্শ দেয়।

মূল উপলব্ধি: slicing কোনো bug নয়, value semantics-এর যৌক্তিক ফল। C++ আপনাকে বেছে নিতে দেয় — value-এর গতি আর সরলতা, নাকি reference-এর নমনীয়তা; ভুল হয় তখনই, যখন hierarchy-র object-কে না ভেবে value হিসেবে ব্যবহার করা হয়।

অনুশীলন

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

  1. আউটপুট অনুমান: struct Log { Log(){ println("Log+"); } ~Log(){ println("Log-"); } };, struct Base { Base(){ println("Base+"); } ~Base(){ println("Base-"); } };, struct Derived : Base { Log log; Derived(){ println("Derived+"); } ~Derived(){ println("Derived-"); } };. main-এ Derived d; — পুরো আউটপুট কোন ক্রমে?

    Base+, Log+, Derived+, তারপর Derived-, Log-, Base-. নিয়ম: আগে base অংশ, তারপর derived-এর সদস্যরা (log), সবশেষে derived-এর body. ধ্বংসে ঠিক উল্টো — body-র destructor, তারপর সদস্য, তারপর base.

  2. is-a নাকি has-a? কারণসহ বলুন কোথায় inheritance আর কোথায় composition: (ক) Manager ও Employee (খ) Car ও Engine (গ) Stack ও std::vector (ঘ) CurrentAccount ও Account (ঙ) Square ও Rectangle, যেখানে Rectangle-এর প্রস্থ-উচ্চতা আলাদা বদলানো যায়।

    (ক) inheritance — ম্যানেজার সত্যিই এক ধরনের কর্মচারী, Employee& নেয় এমন কোড (বেতন-তালিকা, উপস্থিতি) ম্যানেজারের জন্যও চলে। (খ) composition — গাড়ির একটি ইঞ্জিন আছে। (গ) composition — Stack-এর ভেতরে একটি vector; Stack এক ধরনের vector হলে বাইরের কেউ insert দিয়ে মাঝখানে ঢোকাতে পারত, stack-এর নিয়ম ভাঙত। (ঘ) inheritance — চলতি হিসাব এক ধরনের হিসাব। (ঙ) inheritance নয় — Liskov ভাঙে; দুজনকে একটি সাধারণ Shape-এর সন্তান করুন বা আকার immutable রাখুন।

  3. বাগ শিকার: একটি AI এই কোড দিয়েছে — class Manager : Employee { ... };, std::vector<Employee> staff; staff.push_back(Manager{"Rafi", 60000, 5});, for (auto e : staff) total += e.baseSalary();. কয়টি সমস্যা খুঁজে পান?

    তিনটি। (১) class Manager : Employee — class-এ ডিফল্ট private inheritance, তাই বাইরে থেকে Manager-কে Employee হিসেবে ব্যবহারই করা যায় না; push_back লাইনটি compile হবে না। : public Employee লিখুন। (২) সেটা ঠিক করলেও std::vector<Employee>-এ ঢোকানোয় slicing — Manager-এর teamSize হারায়। std::vector<std::unique_ptr<Employee>> আর std::make_unique<Manager>(...) ব্যবহার করুন। (৩) for (auto e : staff) প্রতিটি উপাদান কপি করে; for (const auto& e : staff) লিখুন (unique_ptr-এর ক্ষেত্রে তো কপি compile-ই হবে না)। আর Manager-এর আসল বেতন (ভাতা সহ) পেতে পর্ব ২৫-এর virtual ফাংশন লাগবে।

  4. নিজে লিখুন (কোড): উপরের inheritance_order.cpp-এ StudentSavings-এর নিচে আরেকটি স্তর যোগ করুন: struct ScholarSavings : StudentSavings { using StudentSavings::StudentSavings; ~ScholarSavings() { std::println("Scholar dtor"); } };, আর main-এ ScholarSavings s("Rina"); লিখুন। চালানোর আগে অনুমান করুন — আউটপুটে Scholar-এর কোনো ctor লাইন আসবে কি?

    আসবে না, কারণ ScholarSavings নিজের কোনো constructor body লেখেনি — using দিয়ে StudentSavings-এর constructor নিয়ে এসেছে। আউটপুট: Account ctor: Rina, Savings ctor: rate 5.5%, Student ctor, --- using Rina ---, Scholar dtor, Student dtor, Savings dtor, Account dtor: Rina. ধ্বংসের সময় এবার চার স্তর, আর সবচেয়ে নিচের স্তর আগে। StudentSavings-এর constructor explicit ছিল, উত্তরাধিকারে আসা constructor-ও explicit থাকে — তাই ScholarSavings s = "Rina"; লিখলে compile হতো না।

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

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

ক্রমC++-এ Account থেকে Savings, আর Savings থেকে StudentSavings derive করা। main-এ একটি local StudentSavings object তৈরি হয়ে scope শেষে ধ্বংস হয়। প্রতিটি constructor আর destructor-এর body কোন ক্রমে চলে, সাজান (▲▼ চেপে সরান)

  1. Account-এর constructor body
  2. Savings-এর constructor body
  3. StudentSavings-এর constructor body
  4. StudentSavings-এর destructor
  5. Savings-এর destructor
  6. Account-এর destructor

C++-এ class B { public: void f(); }; class D : B { }; লেখা হলো। বাইরের কোডে D d; d.f(); লিখলে কী হয়?

  • ঠিকমতো চলে, কারণ f() B-এ public
  • Compile error — class-এ ডিফল্ট private inheritance, তাই f() বাইরে থেকে অপ্রবেশ্য
  • Compile হয়, কিন্তু চালালে crash করে
  • ডিফল্ট protected inheritance, তাই চলে

C++-এ struct A থেকে struct B, আর B থেকে struct C derive করা; প্রতিটির constructor একটি করে লাইন প্রিন্ট করে। C c; লিখলে constructor-এর body কোন ক্রমে চলে?

  • A, তারপর B, তারপর C
  • C, তারপর B, তারপর A
  • শুধু C — base constructor আলাদাভাবে ডাকতে হয়
  • ক্রমটি compiler-এর ওপর নির্ভর করে

C++-এ base class-এ void show(int) আর void show(const std::string&) আছে। public derived class শুধু void show(int) ঘোষণা করে, কোনো using নেই। derived object-এ d.show(std::string("hi")) লিখলে কী হয়?

  • base-এর show(const std::string&) চলে
  • string-কে int-এ রূপান্তর করে derived-এর show(int) চলে
  • Compile error — derived-এর show নামটি base-এর সব show overload ঢেকে দিয়েছে
  • Undefined behavior

C++-এ Animal-এর একটি virtual ফাংশন sound() আছে, যা Dog override করে। void play(Animal a) { a.sound(); } ফাংশনে একটি Dog object পাঠালে কোন sound() চলে, আর কেন?

  • Dog-এর, কারণ ফাংশনটি virtual
  • Compile error, Dog-কে Animal-এ কপি করা যায় না
  • Undefined behavior, কারণ Dog-এর অংশ হারিয়েছে
  • Animal-এর, কারণ by value পাঠানোয় object slicing হয়ে a একটি সাধারণ Animal

C++-এ Student আর Staff দুটিই Person থেকে virtual inheritance দিয়ে derive করা, আর TA derive করে Student ও Staff দুটি থেকেই। একটি TA object-এর ভেতরে কয়টি Person অংশ থাকে?

  • শূন্য — virtual base কোনো জায়গা নেয় না
  • একটি — virtual inheritance-এ শেয়ার করা base অংশ একটিই থাকে
  • দুটি — Student আর Staff-এর জন্য একটি করে
  • তিনটি — TA-র নিজেরও একটি

C++-এ class Money final { }; ঘোষণার পরে class Taka : public Money { }; লিখলে কী হয়?

  • শুধু একটি warning আসে, কোড চলে
  • Taka তৈরি হয়, কিন্তু Money-র কোনো সদস্য বদলাতে পারে না
  • Compile error — final class থেকে derive করা যায় না
  • Inheritance নিজে থেকে private হয়ে যায়

শব্দকোষ

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

মনে রাখুন

  1. Inheritance = is-a প্রতিশ্রুতি: derived object base-এর জায়গায় নিশ্চিন্তে বসতে পারবে (Liskov). শুধু কোড ধার নিতে চাইলে composition.
  2. তৈরি: base → derived-এর সদস্য → derived-এর body; ধ্বংস ঠিক উল্টো। base constructor ডাকুন initializer list থেকে; নতুন data না থাকলে using Base::Base;.
  3. data private, derived-দের জন্য দরকারে protected ফাংশন — protected data নয়।
  4. দুই ফাঁদ: একই নামের ফাংশন base-এর সব overload ঢাকে (using Base::f;); by value কপিতে slicing — hierarchy-র object পাঠান const Base& দিয়ে, রাখুন std::vector<std::unique_ptr<Base>>-এ।
  5. প্রায় সবসময় public inheritance (class-এ লিখতে ভুলবেন না); private মানে “implemented in terms of”, diamond দেখলে নকশা আবার ভাবুন, আর base হওয়ার জন্য নকশা না করা class-কে final করুন।

পরের পর্বে: slicing-এর উদাহরণে এক ঝলক দেখেছেন virtual কী করতে পারে। এবার পুরো গল্প — একটি Account& দিয়ে ডাকলেও কীভাবে Savings-এর নিজের ফাংশন চলে, override কেন লিখবেন, virtual destructor কেন বাধ্যতামূলক, আর vtable-এর ভেতরে কী আছে।

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

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