С какими проблемами мы сталкиваемся при множественном наследовании?
v Конфликт имен
v Виртуальные методы.
Конфликт имен решается через явное указание имени базы или через приведение к ссылке на базу.
Но существует ещё одна проблема – эффективность динамического полиморфизма (виртуальных функций)
А теперь рассмотрим множественное
class A
{
public:
A(){};
virtual void a(){ a1 = 1;};
virtual void second(){..;}
int a1, a2, a3;
};
class B
{
public:
B(){};
virtual void bar(){};
virtual void bbar(){};
int b1, b2, b3;
};
class C: public A
{
public:
C(): A(){};
virtual void goo(){};// Собственная новая виртуальная функция
void a(){}; // переопределение
void bar();// переопределение
int c1;
};
….
C c;
Тут надо обратить внимание на следующее:
• Таблица виртуальных методов самого нижнего класса в иерархии доступна через первый указатель vptr.
• Каждый подобъект, который содержит виртуальные методы, имеет свою таблицу виртуальных функций.
Если в классе C переопределить метод, то в соответствующую ячейку в таблице родительского объекта будет записан указатель на новый метод. Если же в классе C добавляются новые функции – они дописываются в конец первой таблицы.
Такой алгоритм становится понятен, если рассмотреть возможные преобразования типов:
• С -> A. Через указатель на класс A можно вызывать только методы, которые прописаны в этом классе.
• C -> B. Ситуация аналогична, только мы можем вызывать виртуальные методы, определенные в классе B.
Новые виртуальные методы (которых нет в родительских классах) можно использовать только через указатель на класс C. В этом случае всегда используется первая таблица виртуальных функций.
Сложность реализации заключается в следующем:
Во время преобразования типов меняется адрес указателя:
|
C c;
B *p = &c;
Указатель p будет содержать адрес объекта c + смещение подобъекта B. Т.е. все вызовы методов через такой указатель будут использовать вторую таблицу виртуальных методов объекта C. Но ведь в такой ситуации при вызове переопределённой в C функции через указатель на B в эту функцию передастся неправильный указатель this! Он будет указывать не на C, как это нужно, а на B.
Приходится расширять таблицу виртуальных функций добавлением в неё смещения от указателя на объект класса до таблицы виртуальных функций для каждой функции. Если виртуальная функция из B переопределена в C, то для неё такое смещение будет равно (-смещение подобъекта B). Если же не была переопределена, то оно будет равно нулю. Для всех виртуальных функций из класса A это смещение будет нулевым, т.к. указатель на подобъект A совпадает с указателем на весь объект C(объект А находится в начале
объекта C). Теперь в функцию можно передать правильный указатель:
this = current_this + offset
где current_this – на подобъект, через который вызывается функция. offset – значение, которое берётся из расширенной таблицы виртуальных функций.
Без наследования по данным таких проблем не возникает, т.к. указатель на таблицу виртуальных функций всегда один.
Ромбовидное и не ромбовидное наследование
Не ромбовидное:: В объекте Z будет два экземпляра объекта A с разными реализациями таблицы виртуальных функций
сlass A{..;}
class X:public A{ …; }
class Y:public A{…; }
class Z: public X, public Y {…;}
Ромбовидное: В объекте Z будет только один экземпляр объекта A
сlass A{..;}
class X: public virtual A{ …; }
class Y: public virtual A{…; }
class Z: public X, public Y {…;}
|
13. Динамическая идентификация типа
Понятие о динамической идентификации типа (ДИТ). Достоинства и
недостатки использования ДИТ. Особенности ДИТ в современных языках
программирования.
14. Понятие о родовых объектах. Обобщенное программирование
Понятие о статической параметризации и родовых объектах.
Достоинства статической параметризации. Статическая параметризация и
ООП.
Родовые модули и подпрограммы в языке Ада.
Порождение нового пакета порождало новый экземпляр данного типа.
package Stack – два параметра – тип элемента размер стека:
generictype T is private;size: integer;package Stack isPushPopend Stack;Можно ли так запрограммировать процедуры, чтобы они эффективно работали для любого размера стека? Конечно, да!
Конкретизация:
package IStack is new Stack(integer, 128);Тривиальная реализация – простая макроподстановка integer и 128. Плюс – крайняя простота. Минус – очень сильное разбухание кода – сколько объявлений, столько и различных процедур.
В Аде большое разнообразие типов формальных параметров родового модуля. Формальные параметры родовых модулей:
параметр-переменная <=> любое константное выражение
type T is private <=> любой тип с операцией присваивания
type T is range <=> любой дискретный тип с упорядоченностью и функциями «следующий» и «предыдущий»
type T is delta <=> любое плавающее выражение
< > <=> при конкретизации процедуры мы можем не указывать этот вариант (параметр по умолчанию).
Пусть у нас есть:
procedure StrSort is new G_SORT(String, Integer, TARR);C помощью < > компилятор находит функцию < для строк и подставляет как параметр по умолчанию. Родовые сегменты – абсолютно необходимая в Аде конструкция, потому что там нет передачи процедур и функций по параметру. Компилятор должен видеть не только спецификацию данной абстракции, но и тело. Гибкость повышается, но тело родовой абстракции должно быть доступно в любой момент конкретизации. В Аде-83 механизм родовых модулей – единственный, который поддерживал ОО. Как вы думаете, что самое главное, что появилось в Аде-95? Правильно, класс – тегированная запись:
|
Механизм шаблонов в языке Си++. Шаблоны-классы и шаблоны-
функции. Параметры шаблонов. Вывод параметров шаблонов. Генерация
кода по шаблонам. Частичная специализация шаблонов. Обобщенное
программирование на языке Си++: функторы, свойства, стратегии, шаблоны
выражений.
Vector <const char *>a;Sort(a);Будет подставлена функция < для указателей, и ошибки в программе не будет, но работать она будет неправильно.
template <class T> bool less (T& x, T& y) { return x<y;}Это описание шаблонной функции. Хорошая функция сортировки должна работать через шаблонную функцию сравнения. Но проблема со * остаётся. Что делать? А вот что.
Пишем:
template <> bool less <const char *> (const char *p1, const char *p2) { return strcmp(p1,p2);} template <> class Vector <void *>;Подставь вместе T void * и сгенерируй код. (?)
Для векторов возможны такие реализации:
1) template <class T> class Vector {…}
2) template <> class Vector <T*>;</br>
Оба по первой реализации и разные.
Vector <const char *> v2;Vector <int *>v3;Оба по второй и одинаковые.
Будет также эффективно, как и встроенный в язык вектор. Механизм частичной специализации очень эффективен. Вспомним:
Find(It first, It last, …)Для каждого конкретного типа можно написать частичную специализацию Find
· I == X (?)
Можно передавать третьим параметром compare, и вызывать но это неэффективно, так как это срывает работу конвейера. Выход – функтор. Перекрываем операцию ():
сlass Finder { public: bool operator ()(T&x, …) { … }}Сравнение механизма шаблонов Си++ и родовых объектов Ады.
Особенности родовых объектов в языках С# и Java.
C#, Java – обобщенные классы
У них очень простой синтаксис и похожая идея:
class Stack <T> {…}interface IComparable <T> {…}Параметризовать можно как классы, так и интерфейсы и методы:
public void add <T> (T x) {…}Слово class просто синтаксически не нужно. Компилятор производит первичный синтаксический анализ и перевод в MIL/Байт-код. Конкретизация же происходит при выполнении кода. Код генерируется оптимальным образом.
В Java в качестве параметризованных типов только классы. Вместо Stack <int> S надо Stack <Integer> S
В C# на такое пойтить не могли. Там же есть структуры! Если аргумент – класс или интерфейс, код разный. Для ссылок одинаковый.
В Java: if (Stack<Integer>.getClass==Stack<String>.getClass()) даст true
В C#:
сlass Dictionary <K, V> {}K – тип ключа, V – тип значения.
Явно при реализации должно быть нечто вроде:
public void Add(K Key, V Value) { … if (K<K1) … …}На это будет выдана ошибка. Потому что функции < может не быть.
Надо:
if ((IComparable)K<K1)Вместо этого можно:
class Dictionary <K, V> where K:IcomparableКонструкция where используется в языке C# для определения ограничений на параметры родовых (другое название - обобщенных) конструкций.
Ее вид: where имя_типа: список_ограничений.
Виды ограничений:
– интерфейс — означает, что параметр-тип должен реализовывать
этот интерфейс;
– имя класса (может быть только одно такое ограничение в списке)
— означает, что параметр-тип должен быть наследником этого класса;
– struct или class – означает, что параметр-тип должен быть
структурой или классом;
– new() - означает, что параметр-тип должен иметь конструктор
умолчания (без параметров).