лекции (2007) (1160825), страница 14
Текст из файла (страница 14)
istream ostream
\ /
iostream
Если просто унаследовать istream и ostream от ios, а потом унаследовать от них iostream, то в iostream будет два поля fd (для файлового дескриптора), которые будут использоваться раздельно. Это неправильно, необходим именно один файловый дескриптор и на чтение, и на запись. В связи с этим введёно понятие виртуального наследования:
class ios {...};
class istream : virtual public ios {...}
class ostream : virtual public ios {...}
class iostream : public istream, public ostream {...}
При виртуальном наследовании как раз получится ромбовидная схема. Основной недостаток этого механизма состоит в том, что при наследовании необходимо не забыть ставить "virtual" там, где это нужно, и не ставить его там, где это не нужно. Если иерархия классов сложная, то такой механизм становится неудобно использовать.
В языках C# и Java множественное наследование поддерживается ограниченно: поддерживается наследование от одного произвольного класса и нескольких интерфейсов. Таким образам, наследование по данным в этих языках может быть только единичным.
5.2. Механизмы динамической идентификации типов (RTTI)
RTTI - RunTime Type
- Information
- Identification
Как уже говорилось ранее, объекты ссылочных типов помимо статического типа имеют ещё и динамический тип. Вернёмся к примеру с графическим редактором и предположим, что необходимо перерисовать некоторый набор фигур разных видов. Таким образом, функция отрисовки на вход получает некоторую коллекцию объектов разных типов и по очереди их отрисовывает, причём все эти объекты должны быть фигурами (то есть типы, производные от "Figure"). Но что будет, если в коллекцию случайно попадёт элемент, не являющийся фигурой? Поскольку это никак не контролируется, в программе произойдёт ошибка. Таким образом, иногда встречаются ситуации, когда необходимо по ходу программы уметь определять динамический тип объектов (зачастую в целях безопасности). Поэтому практически во всех современных ЯП реализованы механизмы динамической идентификации типов.
Минимальный механизм реализован в языке Oberon; он состоит из трёх частей:
1) Проверка типа - это механизм, позволяющий как бы "сравнить" два типа. В Oberon проверка типа имеет вид "t is T" (объект t должен обладать динамическим типом); это логическое выражение, которое даст истину, если объект t в качестве динамического типа имеет тип T или производный от T, и ложь в противном случае. Проверка динамического типа будет выглядеть следующим образом:
IF t IS T THEN
...
ELSE
...
END;
2) Страж типа - это механизм, позволяющий осуществлять контроллируемое преобразование типов, если оно возможно; если нет, генерируется исключительная ситуация. Выглядит это так:
TYPE T = RECORD
i : INTEGER;
END;
TYPE T1 = RECORD
j : INTEGER;
END;
PROCEDURE P(VAR X : T);
...
BEGIN
...
X(T1).j;
...
END;
Здесь выражение "X(T1)" - это страж типа. То есть, если X имеет тип T или производный от него, то произведётся безопасное преобразование типа и появится доступ к полю j, иначе будет возбуждено исключение. Таким образом, часто проверку и преобразование типов осуществляют следующим образом:
PROCEDURE DRAW (VAR F : Figure)
BEGIN
IF F IS Line THEN
DrawLine(F(Line))
ELSIF F IS Point THEN
DrawPoint(F(Point))
END;
END;
Но это неэффективный вариант - получается, что проверка динамического типа производится второй раз при каждом обращении к объекту. Для оптимизации таких случаев предназначена ещё одна конструкция языка - групповой страж типа.
3) Групповой страж типа - развитие идеи обычного стража типа. Проверка производится один раз, и если она проходит, то в данном блоке больше проверок не производится. Это немного напоминает присоединение в языке Pascal или Modula-2, синтаксис следующий:
WITH X : T DO
...
END;
В рассматриваемом примере процедуру перерисовки можно переписать следующим образом:
PROCEDURE DRAW (VAR F : Figure)
BEGIN
IF F IS Line THEN
WITH F : Line DO
DrawLine(F)
END;
ELSIF F IS Point THEN
WITH F : Point DO
DrawPoint(F)
END;
END;
END;
Хотя в данном примере использование группового стража типа не даёт выигрыша в производительности (всё равно будет производиться две проверки), он бывает удобен, если производится много обращений к объекту.
Теперь рассмотрим средства динамической идентификации типов в других языках.
Для простой проверки типа в Delphi используется конструкция "t is T", в Java - "t instanceof T". В Java и C# есть аналоги стража типа: в Java - "T(t)", в C# - "(T)t" (то есть преобразования типов контролируемые). В Delphi же можно производить как контролируемое, так и неконтролируемое динамическое преобразование типов:
1) Контролируемое:
e := t as T;
2) Неконтролируемое:
e := T(t);
Кроме того, в Delphi можно организовать аналог группового стража типа, используя следующую конструкцию:
with t as T do
begin
...
end;
Как уже говорилось ранее, в языках C#, Java и Delphi присутствует дерево типов; другими словами, в этих языках есть базисный класс, называемый Object или TObject. В этом классе могут содержаться дополнительные средства выдачи информации о типе. Более того, к языкам C# и Java применимо понятие "рефлексии" - оно означает, что вся информация о типе содержится в оттранслированном коде и также может использоваться.
Наконец, рассмотрим механизм динамической идентификации типов в языке C++. Здесь условия на тип для произведения идентификации несколько строже: объект класса не только должен обладать динамическим типом (то есть быть указателем или ссылкой), но этот класс также должен иметь хотя бы один виртуальный метод (это требование связано с тем, что у класса должна существовать ТВМ, содержащая необходимую информацию о нём). В общем-то, это не ослабляет механизм RTTI, поскольку именно для классов с виртуальными методами и нужна проверка.
1) Динамическая идентификация типов в C++ производится с помощью конструкции "dynamic_cast <T> (e)". Она одновременно работает и как проверка типа, и как страж типа: если преобразование возможно, то dynamic_cast выдаст преобразованный указатель или ссылку; если это невозможно, то вместо указателя будет выдан NULL, а в случае со ссылкой будет возбуждено исключение "bad_cast".
2) Для ссылок и указателей существует и небезопасное преобразование - "static_cast". Фактически, "static_cast <T> (e)" означает тоже самое, что и "(T)e".
3) Преобразование "reinterpret_cast" позволяет приводить друг к другу несовместимые типы. Контроль, естественно, не осуществляется.
4) Наконец, преобразование "const_cast" позволяет насильно снять константность. Это может быть полезно, если, например, есть константная ссылка на динамический объект.
Кроме преобразований, для получения информации о типе в языке C++ можно использовать специальный псевдокласс type_info и псевдофункцию typeid(e), которая возвращает ссылку на type_info для динамического типа выражения e. Принадлежность объекта конкретному типу можно проверить сравнением ссылок, выдаваемых typeid(): "typeid(t)==typeid(T)".
Глава 6. Статическая параметризация.
Статическая параметризация означает, что связывание параметров (где бы то ни было, в разных языках реализовано по-разному) будет происходить не динамически, а ещё на этапе трансляции. Такая схема часто бывает удобна: например, статическая параметризация типов позволяет избежать накладных расходов на динамическую идентификацию типов. Более того, параметризовывать можно не только типы, но и любые объекты данных (в том числе функции, так как во многих языках есть процедурный тип данных). Простой пример - стек; если необходимо иметь стеки для разных типов элементов и к тому же разной длины, то, используя механизм статической параметризации, можно описать стек, используя пока неизвестный тип объектов и длину стека в виде формальных параметров, а затем при создании каждого нового экземпляра стека указывать тот конкретный тип элементов и ту его длину, которые в данный момент необходимы.
Статическая параметризация появилась в языках программирования достаточно давно - начиная со стандарта Ada-83. В языке C++ она появилась только в начале 90-х годов в виде механизма шаблонов. Как уже было сказано, Бьёрн Страуструп думал о включении шаблонов в C++ ещё в середине 80-х годов, но тогда предпочёл реализовать множественное наследование. В своей книге "Дизайн и эволюция языка С++" Страуструп признаёт это решение одной из немногих своих ошибок. Дело в том, что из-за позднего введения в язык шаблонов позже появилась и библиотека стандартных шаблонов STL, которая только сравнительно недавно завоевала достаточную популярность. Наконец, в языках C# и Java статическая параметризация появилась только в 2004 и 2005 годах соответственно, в виде родовых классов и методов.
Итак, начнём рассмотрение статической параметризации с языка Ada. Для этого механизма в языке было введено новое ключевое слово - "generic"; genegic-модуль называют также "родовым модулем". Для примера возьмём всё тот же стек:
generic
type El is private;
SIZE : integer;
package G_STACKS is
type Stack is private;
procedure Push (S : in out Stack; X : in El);
procedure Pop (S : in out Stack; X : out El);
...
end G_STACKS;
package body G_STACKS is
procedure Pop (S : in out Stack ; X : out T) is ...
procedure Push (S : in out Stack ; X : in T) is ...
private
type Stack is record
body ; array (1..SIZE) of El;
top : integer := 1;
end record;
end G_STACKS;
Здесь используются два параметра - тип элементов стека и длина стека. Соответственно, для создания нового экземпляра стека необходимо создать новый пакет, определив эти два параметра:
package InitStack128 is
new G_STACKS(integer, 128);
Несомненный плюс такого подхода - простота. Но за нёё приходится расплачиваться большим объёмом кода при решении сравнительно небольших задач. Кроме того, часто вриходится вводить большее количество родовых параметра, чем это необходимо. Пример - написание функции сортировки для произвольного типа данных:
generic
type El is private;
type INDEX is range <>;
type ARR is array (INDEX range <>) of El;
with function "<"(X, Y : El) return boolean;
procedure G_SORT(A: in out ARR; L,R : INDEX);
procedure G_SORT(A: in out ARR; L,R : INDEX) is
begin
...
end;
...
private Comp(S1, S2 : String) with function "<" (X, Y : El) return boolean is
begin
...
end;
...
procedure SSort is new G_SORT(String, AS, integer, Comp);
В языке Ada существует огромное разнообразие типов формальных параметров в родовых модулях:
type T is private; -- любой тип с операцией присваивания
type A is array (T1) of E; -- любой массив с индексом типа T1 и элементами типа E (I и E могут быть формальными параметрами)
type IND is range <>; -- любой знаковый целый тип
type I is digits <>; -- любой тип с плавающей точкой
...
Перейдём к рассмотрению средства статической параметризации в языке C++ - механизму шаблонов. Принцип действия тот же самый, синтаксис достаточно простой:
template <список аргументов>
объявление класса или функции;
В C++ есть только три вида формальных параметров: тип, функция или объект данных. Шаблонными могут быть функции и классы. Вот как, например, будет выглядеть шаблонный класс "стек":
template <class T, int size>
class Stack
{
T body[size];
int top;
public:
Stack() { top = 0;}
void Push(T X) { body[top++] = X;}
...
}
Соответственно, создать экземпляр стека можно следующим образом:
Stack <int, 128> S1;
Stack <int, 64*2> S2;
Можно задавать формальные параметры по умолчанию; например, в библиотеке стандартных шаблонов STL при создании контейнеров, как правило, можно не указывать распределитель памяти - аллокатор, тогда будет использоваться стандартный. Например, так выглядит спецификация шаблона для класса vector:
template <class T, class Allocator - allocator<T>> class vector;
В языках C# и Java недавно были введены так называемые родовые классы и методы ("generics"). Синтаксис следующий:
class X <T> {...}
class Y <T> {...}
interface Y <T> {...}
void f <T> (T i) {...}
При этом в Java в качестве формальных параметров могут выступать только классы, в C# параметрами могут быть и типы. Если использовать родовые классы и методы вкупе с наследованием, то получается очень мощный механизм.
Для вас трудились: zvoice, twinka & DiMan