5. Множественное наследование.

Прямая и косвенная базы. Виртуальное наследование. Понятие доминирования. Порядок создания и уничтожения объектов. Проблемы множественного наследования. Неоднозначности при множественном наследовании.

Множественное наследование

Язык С++ – это практически единственный язык, в котором присутствует множественное наследование.

Преимущества множественного наследования

В каких случаях мы выделяем общую базу, если у двух разных понятий общее схема использования, обязаны объявить общим базовым классов, причем базовый класс будет задавать интерфейс, позволит подменять один объект на другой, например, автобус, трамвай разные объекты, но схема использования общая.

Рассмотрим разные ситуация, когда у нас один и тот же набор методов, но схема использования разная или частично набор методов.

  1. общая схема использования

  2. общие методы

  3. общая реализация методов или частично.

Примечание: Изначально выделять базовые понятия на ранней стадии. Должны писать так код, чтобы при модификации не изменять написанный код. Если понятия выполняют несколько ролей, то должны такое разбивать. Понятие выполнять одну роль. Не надо на роль возлагать несколько ответственностей - должны расщеплять класс.

Какие преимущества множественного наследования? Пойдем методом от противного - попробуем избавиться от множественного наследования, и представим, что было бы.

Представим такую ситуацию: выстраиваем вертикальную иерархию, класс C наследуется от класса B, а B наследуется от класса A. В этом случае, в класс A мы должны вынести много того, что к понятию класса А не относится. Не совсем логично.

В случае со множественным наследованием, мы четко разделяем понятия A и B. Такой подход уменьшает иерархию.

Второй момент (опять не используем множественное наследование). Мы можем не выносить что-то в базовый класс, а один из классов включить как подобъект, то есть не использовать наследование. Пусть С - производная от класса А и включает подобъект класса В. Тоже возникнет проблема - не будем иметь доступа к защищенным полям класса В (нам придется делать это через методы класса В) + придется протаскивать интерфейс для класса С класса В.

Понятие прямой и косвенной базы

Непосредственная база для какого-либо класса является прямой базой. Может входить в производный класс только один раз. Прямой базовый класс явно перечисляется в заголовке при объявлении производного класса. Производный класс может входит один раз.

Косвенная база - прямая база прямой базы. Может входить в базовый класс сколько угодно раз.

Может возникнуть ситуация, когда в наш класс косвенная база входит два раза:

Схема 1 косвенного наследования

В большинстве случаев нам необходимо, чтобы базовый класс входил в производный только один раз. Включать механизм контролирования, было вхождения или не было. Рассмотрим пример ниже. Использовать виртуально наследование - virtual.

Схема 2 косвенного наследования

Пример. Базовый класс входит в производный два раза

В данном примере два раза отрабатывает конструктор класса А.

class A
{
public:
    A (char* s)
    {
        cout << "Creature A" << s << ";" << endl;
    }
};

class B : public A
{
public:
    B() : A (" from B")
    {
        cout << "Creature B;" << endl;
    }
};

class C : public B, public A // В класс C подобъект класса А будет входить два раза.
{
public:
    C() : A(" from C")
    {
        cout << "Creature C;" << endl;
    }
};

void main()
{
    C obj;
}

Вызовется конструктор класса С. Из С вызовется конструктор класса B (так как класс B наследуется раньше класса A). Вызовется конструктор класса A. Создастся объект класса А. Создастся объект класса B. Из С вызовется конструктор класса А. Создастся объект класса А. Создастся объект класса С.

На экран в результате работы программы будет выведено следующее:

Creature A from B;
Creature B;
Creature A from C;
Creature C;

Проблема решается с помощью виртуального наследования.

Виртуальное наследование

В большинстве случаев необходимо, чтобы базовый класс входил в производный только один раз. Для этого используется виртуальное наследование.

ВАЖНО! При виртуальном наследовании меняется порядок создания объекта: если в списке наследования есть виртуальное наследование (виртуальные базы), они отрабатывают в первую очередь слева направо, а потом всё остальные базы.

Чтобы сделать родительский (базовый) класс общим, используется ключевое слово virtual в строке объявления дочернего класса.

Пример. Базовый класс входит в производный один раз

Исправим предыдущий пример, добавив виртуальное наследование. Теперь, когда для класса С будет создаваться объект класса А, будет включаться механизм виртуальности. Когда будет создаваться подобъект класса С, для него не будет создан объект класса А.

class A
{
public:
    A(char* s)
    {
        cout << "Creature A" << s << ";" << endl;
    }
};

class B : virtual public A
{
public:
    B() : A(" from B")
    {
        cout << "Creature B;" << endl;
    }
};

class C: public B, virtual public A
{
public:
    C() : A(" from C")
    {
        cout << "Creature C;" << endl;
    }
};

void main()
{
    C obj;
}

На экран в результате работы программы будет выведено следующее:

Creature A from C;
Creature B;
Creature C;

Проблемы виртуального наследования

Рассмотрим следующий пример:

class A {};

class B : virtual public A{}; // Здесь virtual наследование

class C : public A {}; // Здесь не virtual наследование

class D : public B, public C{}; // так сначала обращение к B, то используется схема 1, иначе схема 2.

Порядок создания объекта класса D: сначала вызывается конструктор класса B. Для него вызывается конструктор класса А, будет выполняться механизм виртуальности. Создастся подобъект класса А, отработает конструктор класса B. Для C уже не будет выполняться A.

Но если поменять порядок наследования для класса D:

class D : public C, public B{};

Смена последовательности наследование приводит к тому, что класс А будет включен два раза, что не должно происходить при включении механизма виртуальности.

ВАЖНО! Используя множественное наследование, виртуально надо стараться виртуально наследоваться по всем ветвям, чтобы не зависеть от порядка наследования.

Ниже представлена правильная версия:

class A {};

class B : virtual public A{}; // Здесь virtual наследование

class C : virtual public A {}; // Тут теперь тоже virtual наследование

class D : public B, public C{}; // в любом случае схема 2

Доминирование

В общем случае, диаграмма наследования представляет собой направленный ациклический граф.

ВАЖНО! Метод, находящийся в шаге дальше, подменяет другие методы, находящиеся выше.

Рассмотрим нижеприведенную схему. В классе А есть перегруженный метод f: f() и f(int). В производном классе есть всего один метод f().

class A
{
public:
   void f();
   void f(int);
   void g();
};

class B: public A
{
public:
   void f(); 
};

B obj;
obj.f(); // вызывается метод из B
obj.f(1); // ошибка из-за подмены функции f()

В данном случае в классе B подменяется метод f(). Для объектов класса B метод f(int) недоступен. Это сделано, чтобы при проектировании было корректное наследование.

ВАЖНО! Если мы подменяем один перегруженный метод, мы обязаны подменить все остальные. Если мы этого не сделаем, они будут перекрыты.

Пример. Использование using

Можно использовать using (но это плохо). В данном примере мы хоть и описали в классе B только один перегруженный метод, но с помощью using мы можем использовать f(int). Таким образом, объект класса B может использовать f() своего класса B и f(int) базового класса A.

class A
{
public:
	void f() { cout<<"Executing f() from A;"<<endl; }
	void f(int i) { cout<<"Executing f(int) from A;"<<endl; }
};

class B : virtual public A
{
public:
	void f() { cout<<"Executing f from B;"<<endl; }
	using A::f; // плохо!!!
};

class C : virtual public A
{
};

class D : virtual public C, virtual public B
{
};

void main()
{
	D obj;

	obj.f();  // вызывается метод f() класса B
	obj.f(1); // Вызывается метод f(int) класса A - Error не используя using
        obj.A::f(1); // так нельзя делать плохо!!!
        obj.g(); // легко вызывается так этот метод не подменяется
}

Программа выведет на экран:

Executing f from B;
Executing f(int) from A;

Пара схем из мира доминирования

Какие схемы возможны при множественном наследовании?

В данном случае для объектов класса D метод f() класса B подменят метод f() класса A.

D obj;
obj f(); // Вызовется f() из B

Аналогично для этой схемы:

Подмена метода рассматривается в примере ниже.

Пример. Подмена метода f()

class A
{
public:
    void f() { cout<<"Executing f from A;"<<endl; }
};

class B : virtual public A
{
public:
    void f() { cout<<"Executing f from B;"<<endl; }
};

class D : public B, virtual public A
{
};

void main()
{
    D obj;
    obj.f(); // вызывается метод из B
}

В данном примере метод f() класса В доминирует для класса С над методами класса А.

Программа выведет на экран:

Executing f from B;

В современных языках от множественного наследования отказались. В объектно-ориентированном проектировании множественное наследование не рассматривается.

====>

Проблемы, возникающие с множественным наследование.

Множественный вызов методов

Рассмотрим следующую схему:

Предположим, есть объект класса А с методом draw(), который умеет себя нарисовать. Производные от него классы - B и C, тоже имеют метод draw() и тоже могут себя нарисовать, а так же они могут нарисовать подобъект базового класса. То есть, при рисовании объектов класса B (аналогично для C) вызывается метод draw() класса А.

Когда мы создаем объект класса D, в котором мы должны отрисовать объект класса B и объект класса C, draw() класса A вызывается два раза. Это называется проблема множественного вызова базового класса.

Пример. Множественный вызов методов

class A
{
public:
	void f() { cout<<"Executing f from A;"<<endl; }
};

class B : virtual public A
{
public:
	void f()
	{ 
		this->A::f();
		cout<<"Executing f from B;"<<endl;
	}
};

class C : virtual public A
{
public:
	void f()
	{ 
		A::f();
		cout<<"Executing f from C;"<<endl;
	}
};

class D : virtual public C, virtual public B
{
public:
	void f()
	{ 
		C::f();
		B::f();
		cout<<"Executing f from D;"<<endl;
	}
};

void main()
{
	D obj;

	obj.f();
}

Метод f() класса А срабатывает дважды.

Программа выведет на экран:

Executing f from A;
Executing f from C;
Executing f from A;
Executing f from B;
Executing f from D;

Красивых способов борьбы с этой чумой, к сожалению, нет.

Пример. Решение проблемы множественного вызова методов

Идея: разделить метод на две части. Часть, которая относится непосредственно к самому классу, и часть, которая относится ко всему объекту.

В классе А мы разделили f() на две части, причем, то что относится к самому классу A - его собственное, мы делаем его защищенным, доступным только для методов класса А и производных классов. В производных классах мы тоже разделяем - метод, относящийся непосредственно к самому классу и ко всему объекту.

class A
{
protected:
    void _f() { cout<<"Executing f from A;"<<endl; }
public:
    void f() { this->_f(); } 
};

class B : virtual public A
{
protected:
    void _f() { cout<<"Executing f from B;"<<endl; }
public:
    void f()
    { 
        A::_f();
        this->_f();		
    }
};

class C : virtual public A
{
protected:
    void _f() { cout<<"Executing f from C;"<<endl; }
public:
    void f()
    { 
        A::_f();
        this->_f();
    }
};

class D : virtual public B, virtual public C
{
protected:
    void _f() { cout<<"Executing f from D;"<<endl; }
public:
    void f()
    { 
        A::_f(); 
        B::_f(); 
        C::_f();
	this->_f();
    }
};

void main()
{
    D obj;

    obj.f();
}

Программа выведет на экран:

Executing f from A;
Executing f from C;
Executing f from B;
Executing f from D;

Решение не самое красивое, но других, к сожалению, нет.

Следующая проблема, которая возникает при множественном наследовании – это неоднозначность при множественном наследовании.

Пример. Неоднозначности при множественном наследовании

class A
{
public:
	int a;
	int (*b)(); // Если что, это указатель на функцию :)
	int f();
	int f(int);
	int g();
};
	
class B
{
	int a;
	int b;
public:
	int f();
	int g;
	int h();
	int h(int);
};

class C: public A, public B {};

class D
{
public:
	static void fun(C& obj)
	{
		obj.a = 1;	// Error!!!
		obj.b();	// Error!!!
		obj.f();	// Error!!!
		obj.f(1);	// Error!!!
		obj.g = 1;	// Error!!!
		obj.h(); obj.h(1); // Только для тех методов, которые идут по одной ветви - всё корректно.
	}
};

void main()
{
	C obj;

	D::fun(obj);
}

Есть два класса – класс А и класс В. Класс С – производная от классов А и В. В классе С мы получаем доступ к членам объекта класса C. Здесь играет следующее правило проверки на неоднозначность: проверка на неоднозначность происходит до проверки на перегрузку, на тип и до проверки на уровень доступа.

Решение проблемы (тоже некрасивое решение): Объединяем два класса, и в производном классе полностью подменяем то, что находится в базовых классах. Получается громоздкий производный класс (ведь мы всё, что относится к базовым классам - подменяем). Когда мы объединяем два класса одним классом, есть еще один недостаток.(см. пример ниже).

Пример. Замена интерфейса

Также есть еще один недостаток – когда мы программируем, для объекта какого-либо класса мы должны выделить цель - для чего мы создаем это понятие. Объекты данного класса должны выполнять определенную задачу, и она должна быть только одна. Правило: У одного объекта не должно быть несколько обязанностей. Когда мы объединяем два понятия, это чаще всего приводит к тому, что новое сформированное понятие имеет несколько обязанностей. Такого быть не должно.

Если один объект выступает в разных ролях, мы не должны объединять интерфейс, должны его разносить.

Исключением является ситуация, когда мы объединяем два разных понятия, формируя интерфейс одной обязанности. В этом случае используется следующая схема. У нас есть два класса, и мы формируем новое понятие, используя интерфейс только одного класса. В данном случае идёт наследование только по схеме public только от класса B, от класса A по схеме private. Таким образом, для объектов класса C интерфейс класса A невидим.

class A
{
public:
	void f1() { cout<<"Executing f1 from A;"<<endl; }
	void f2() { cout<<"Executing f2 from A;"<<endl; }
};

class B
{
public:
	void f1() { cout<<"Executing f1 from B;"<<endl; }
	void f3() { cout<<"Executing f3 from B;"<<endl; }
};

class C : private A, public B {};

class D
{
public:
    void g1(A& obj)
    {
        obj.f1();
        obj.f2();
    }
    void g2(B& obj)
    {
        obj.f1();
        obj.f3();
    }
};

void main()
{
    C obj;
    D d;

    // obj.f1();  Error!!! Неоднозначность
    // d.g1(obj); Error!!! Нет приведения к базовому классу при наследовании по схеме private
    d.g2(obj);
}

Но здесь здесь есть проблема – проверка на неоднозначность происходит до проверки на схему наследования. Поэтому метод f() для объектов класса C мы вызвать не сможем - это неоднозначность, хотя наследуем по разной схеме.

Что нужно сделать: Нужно в классе С подменить те методы, которые идут по ветви public.

При такой схеме (когда в одном случае мы поддерживаем только один интерфейс) множественное наследование можно использовать.

ВАЖНО! Если в наге нет общей базы (общая база задает интерфейсные методы для производных классов), то подмена должны осуществляться только по одной ветке.

Last updated