ارث بری چندگانه در سی++

سی++ یکی از زبان هاییه که این اجازه رو به ما میده تا کلاسی که نوشتیم چندین کلاس والد داشته باشه یا به عبارت دیگه، قابلیت ارث بری چندگانه یا multiple inheritance رو داره.

چطور استفاده می‌شه

خودِ ارث بری چندتایی خیلی عجیب غریب نیست و تنها فرقش اینه که موقع نوشتن کلاس، با استفاده از «کاما» میایم و Base Class هارو جدا می‌کنیم. مثال:

#include <string>
 
class Person
{
private:
    std::string m_name;
    int m_age;
 
public:
    Person(std::string name, int age)
        : m_name(name), m_age(age)
    {
    }
 
    std::string getName() { return m_name; }
    int getAge() { return m_age; }
};
 
class Employee
{
private:
    std::string m_employer;
    double m_wage;
 
public:
    Employee(std::string employer, double wage)
        : m_employer(employer), m_wage(wage)
    {
    }
 
    std::string getEmployer() { return m_employer; }
    double getWage() { return m_wage; }
};
 
// Teacher publicly inherits Person and Employee
class Teacher: public Person, public Employee
{
private:
     int m_teachesGrade;
 
public:
    Teacher(std::string name, int age, std::string employer, double wage, int teachesGrade)
        : Person(name, age), Employee(employer, wage), m_teachesGrade(teachesGrade)
    {
    }
};

اما بیشتر از مزایاش، چالش ها و مشکلاتش قابل بحثه!

مشکل Diamond Problem

این مشکل که به Deadly Diamond of Death هم معروفه، از این قراره که فرض کنید کلاس های B و C دو کلاس مجزا باشند و هر دوتاشون از کلاس A ارث بری کرده باشند. حالا اگر ما کلاسی مثل D داشته باشیم که به شکل همزمان از B و C ارث بری کرده، دوبار کلاس A رو در کلاس D خواهیم داشت که این باعث مشکل می‌شه.

نمایش تصویری Diamond problem. حالا دلیل اسمش معلوم شد؟ 🙂

اگر تابعی مثل print در کلاس A داشته باشیم که در هیچکدوم از ارث بری ها بازنویسی نشده باشه و حالا از طریق کلاس D بخواد فراخوانی بشه چه اتفاقی میوفته؟

به ارور برمیخوریم! چون کامپایلر نمیدونه از کدوم یکی از نسخه های A که الآن در اختیار داره باید استفاده کنه و از کدوم مسیر باید بره.

از طریق کلاس C بره و به A برسه یا از طریق کلاس B ؟

حل مسئه Diamond problem

راه حل اول اینه که دقیقا برای کامپایلر مشخص کنیم از چه مسیری باید به کلاس A برسه. مثال:

class A {
	public:
		print() {}
};

class B : public A {};
class C : public A {};

// multiple inheritance 

class D : public B, public C {};

D object;
// explicitly determine a way to "A"
D.B::print();

یک راه دیگه هم وجود داره.

Virtual Inheritance

با استفاده از ارث بری مجازی، سی++ فقط یک نسخه از A رو به عنوان والد در نظر میگیره و نمیذاره چند نسخه از A بوجود بیاد.

فقط کافیه که ارث بری‌مون رو به شکل virtual انجام بدیم:

class A {
	public:
		virtual print() {}
};

class B : virtual public A {};
class C : virtual public A {};

// multiple inheritance 

class D : public B, public C {};

D object;
// no need to explicit qualification
D.print();

نحوه عمل کردن virtual inheritance بسیار شبیه به Virtual function ها هست که توی این پست درباره‌ش توضیح دادم.

در آخر

همونطور که دیدیم، ظاهرا چالش ها و مشکلات این روش بیشتر از فایده‌ش هست اما جاهایی هم هست که بهترین راهیه که میتونیم ازش استفاده کنیم.

بهتره که تا جای ممکن سعی کنیم راه الگوریتم‌مون رو بدون استفاده از ارث بری چندگانه پیاده سازی بکنیم مگر اینکه پیاده سازی به این روش باعث بشه که پیچیدگی کار کمتر بشه.

نکات جدیدی که از فصل ۱۱ سی++ دایتل یاد گرفتم – ارث بری

این فصل از دایتل درباره‌ی ویژگی ارث‌بری توی سی++ بود. نکاتی که برام جدیده رو توی این پست نوشتم.

تفاوت is-a relationship و has-a relationship

وقتی یک کلاسی(A) از یک کلاس دیگه(B) ارث بری می‌کنه، میگیم که رابطه‌شون از نوع is-a relationship هست. یعنی کلاس A نوعی از کلاس B عه؛ مثالش؟ «خودرو» و «وسیله نقلیه». خودرو از وسیله‌ی نقلیه مشتق شده(نمیدونم این تعبیرِ مشتق شدن درسته یا نه) و تمام ویژگی های یک وسیله‌ی نقلیه رو داره. درواقع، «خودرو یک وسیله‌ی نقلیه‌ست».

اما وقتی یک کلاس در data-member های خود یک شئ از یک کلاس دیگه رو داره(composition) میگیم رابطه‌شون has-a relationship هست. توی مثال خودرو، میشه فرمون خودرو رو مثال زد که خودش یک شئ هست و عضوی از خودرو محسوب میشه(نه نوعی از خودرو!).

Constructor

در حالت عادی، توابع سازنده(constructor)، نابودکننده(destructor) و اوپراتور تخصیص (operator= ) از کلاس والد ارث برده نمیشن.

وقتی یک شئ از کلاس فرزند(B) ساخته میشه اگه کلاس والد(A) دارای default constructor باشه، بدون اینکه کانستراکتور والد رو صراحتا صدا بزنیم، خودش به صورت ضمنی اجرا میشه. اما اگه کلاس A دارای default constructor نباشه باید حتما خودمون کانستراکتور کلاس A رو توی کانستراکتور کلاس B صدا بزنیم.

دسترسی به عناصر private والد

کلاس فرزند نمیتونه به طور مستقیم به عناصر private کلاس والد دسترسی داشته باشه. برای اینکه بشه اینکار رو انجام داد، اون عناصر مورد نظر توی کلاس والد باید به عنوان protected تعریف شده باشن یا اینکه کلاس فرزند از طریق توابع public کلاس والد اقدام به دسترسی گرفتن به عناصر پرایوت بکنه.

با استفاده از protected access specifier به دلیل اینکه میتونید به صورت مستقیم به عناصر مورد دسترسی داشته باشید، برنامه‌تون کمی سریعتر میشه. در مقابل استفاده از توابع public باعث میشه برنامه‌تون کمی کند تر باشه.

اما این روش(protected) مشکلاتی هم داره:

اول اینکه ممکنه داده های invalid بهشون تخصیص داده بشه. یعنی از اونجایی که دسترسی به صورت مستقیم و بدون اعتبارسنجی به اون‌ها وجود داره، ممکنه یک داده‌ی اشتباهی بهشون تخصیص داده بشه.

دوم اینکه برنامه رو شکننده (اصطلاحا fragile) میکنه. یعنی کلاس فرزند جای اینکه بر پایه خدماتی(service) باشه که کلاس والد ارائه میده، بر پایه‌ی پیاده سازی (implementation) کلاس والد توسعه پیدا میکنه. مشکلش چیه؟ از نظر مهندسی نرم افزار بهتره که تغییرات رو localize بکنیم. یعنی برنامه طوری نباشه که اگر اسم یکی از عناصر رو توی کلاس والد تغییر دادیم، مجبور بشیم تمام جاهایی از کلاس فرزندمون که به اون عضو رفرنس دادیم رو تغییر بدیم و استفاده از protected پتانسیل بوجود اومدن چنین مشکلی رو ایجاد می‌کنه.

مگه نگفتی استفاده از توابع public برنامه رو کندتر می‌کنه؟

چرا. اما بهتره که ما اصول مهندسی نرم افزار رو رعایت کنیم و بهینه کردن کد(optimization) توی این مورد رو به دست کامپایلر بسپاریم. چون خیلی وقتا کامپایلر خودش بهینه سازی های خوبی انجام میده(مثلا ممکنه توابع set و get رو به حالت inline دربیاره). به قول معروف، Do not second-guess the compiler .

فرق بین ارث بری public و بقیه

خود کتاب یه جدول خیلی خوب رسم کرده که من عکسش رو میذارم. به نظرم خوندن همون کافیه 🙂

جدولی که توضیح میده هرکدوم از انواع ارث بری public, private, protected چه ویژگی هایی دارن.

ارث بردن constructor

بالاتر گفتم که در حالت عادی کانستراکتور از کلاس والد به ارث برده نمیشه. اما توی C++ 11 ویژگی ای اضافه شده که اجازه میده کانستراکتور والد توسط کلاس فرزند به ارث برده بشه.

برای اینکار باید یه همچین چیزی توی تعریف کلاستون بنویسید:

using BaseClass :: BaseClass ;

که BaseClass اسم کلاس والد هست. با اینکار، کامپایلر به ازای کانستراکتور های موجود توی کلاس والد، یه کانستراکتور توی کلاس فرزند میسازه که به طور خودکار تابع متناظرش توی کلاس والد رو صدا بزنه و data-member های کلاس فرزند رو default initialize بکنه.

چندتا نکته برای این کار وجود داره:

  • کانستراکتور های default, copy, move ارث بری نمیشن.
  • اگر توی کلاس فرزند کانستراکتوری باشه که ورودی هاش با ورودی های یک کانستراکتور توی کلاس والد یکسان باشه، اون کانستراکتور به ارث برده نمیشه.
  • اگه کانستراکتور دارای default argument باشه، به همون صورت به ارث نمیرسه بلکه به صورت چند کانستراکتور overload شده به ارث میرسه.
  • اگه یه کانستراکتور توی کلاس والد حذف بشه، توی کلاس فرزند هم حذف میشه.

در آخر

فصل بعدی درباره‌ی چند ریختی یا همون polymorphism عه که دانش ابتدایی ما برای برنامه نویسی شئ‌گرا رو کامل میکنه.