Сам механизм RTTI не является особенно сложным, однако не всегда понятно, когда и как его нужно применять, поскольку виртуальные функции позволяют в большинстве случаев обойтись без RTTI.
1 Реальным примером такой системы является Windows.
Рассмотрим пример. Предположим, мы разрабатываем файловую систему в составе некоей операционной системы с интерфейсом GUI1 [55, 56, 59]. Для пользователя файлы представляются пиктограммами. Щелкнув на пиктограмме правой кнопкой мыши, мы получаем контекстное меню с командами открытия, закрытия, копирования и т. п. Реализация файловой системы основана на иерархии классов, корнем которой является абстрактный класс File примерно следующего вида:
rtual void open() = 0;
rtual void closeQ = 0;
rtual void read() = 0;
rtual void write() = 0;
virtual ~File() = 0;
};
File::~File(){} // чистый виртуальный деструктор требует реализации
Пусть в системе в первой версии существуют текстовые файлы и двоичные исполняемые файлы. В иерархии классов эти файлы представлены классами-наследниками от базового абстрактного класса, в которых реализованы чистые виртуальные функции. Например, класс BinaryFile определяет двоичный исполняемый файл:
class BinaryFile: public File { public:
void open() { Execute(this); }
// другие методы
};
Класс TextFi le определяет тестовый файл:
class TextFile: public File { public:
void open() { Activate_text_processor(this); } virtual void printO; // другие методы
};
Двоичные файлы, естественно, отличаются от текстовых во многих деталях. Например, при открытии двоичного файла он запускается на выполнение, а при открытии текстового файла запускается текстовый редактор и ему передается текущий открываемый файл. Поскольку у нас функции открытия виртуальные, то проблем с правильным открытием не должно возникать. Однако различие проявляется и в том, что текстовые файлы можно печатать, а двоичные — нет. Следовательно, в классе TextFile существует специфический для этого типа файлов метод printO, которого нет в классе BinaryFile.
Щелчок правой кнопкой мыши на пиктограмме файла должна обрабатывать API-функция, которая открывает упомянутое контекстное меню. Заголовок функции может выглядеть так:
OnRightClick(File &file);
Функция принимает параметр базового класса, следовательно, в соответствии с принципом подстановки, может принимать в качестве аргумента как текстовый, так и двоичный файлы. Однако наши файлы не одинаковы: контекстное меню текстового файла должно отличаться от контекстного меню двоичного файла, по крайней мере, наличием команды печати. Следовательно, функция
OnRightClickO должна «уметь» различать типы файлов во время работы! Это можно обеспечить с помощью оператора typeidO. Структура функции может быть такой:
OnRightClick(File &file)
{ if (typeid(file) == typeid(TextFile))
{ // обработка текстового файла
}
else
{ // обработка двоичного файла }
}
Хотя статическим типом аргумента file является тип File, на место объекта базового класса может быть подставлен объект производного класса. Поэтому оператор typeidO возвращает объект type_info, который несет информацию о реальном типе передаваемого аргумента. Оператор i f проверяет, совпадает ли реальный тип аргумента f i le с типом TextFi le.
Однако одного оператора typeid () может оказаться недостаточно. Пусть у нас в системе появляется третий тип файла: HTML-файл. Так как HTML-файл является текстовым файлом, то, естественно, класс HTMLFile является наследником класса TextFile:
class HTMLFile: public TextFile { public:
void open() { Activate_Brouser(this); } vi rtual void printO ; // другие методы
};
Реализация функции открытия, естественно, отличается от реализации функции открытия базового текстового файла: для HTML-файла вызывается программа-браузер1. Функция печати, очевидно, тоже должна быть реализована по-другому.
В функции OnRi ghtCl i ck () у нас возникают проблемы, так как при передаче аргумента типа HTMLFi le она будет работать по ветке двоичного файла. А все потому, что значение typeid(HTMLFi le) не равно значению typeid(TextFile), хотя типы HTMLFi le и TextFi le — ближайшие «родственники». В такой ситуации нам и может пригодиться оператор dynamic__cast<>. Вместо ссылки будем передавать в функцию OnRightClickO указатель. Тогда функция приобретает следующий вид:
OnRightClick(File *file)
{ TextFile *pFile = dynamic_cast<TextFile *>(file); if (pFile)
{ // обработка текстового файпа и HTML-файла }
else
{ // обработка двоичного файла }
1 Не забывайте, что в базовом абстрактном классе функция открытия определена как чистая виртуальная, поэтому во всех производных классах она тоже является виртуальной, хотя это явно и не указано.
Если в функцию будет передан указатель на HTMLFi 1е, то он успешно преобразуется к типу указателя на TextFi 1е. В этом случае указатель pFi le оказывается ненулевым. Если же при вызове функции будет передаваться аргумент-указатель на двоичный файл, то преобразование не выполнится и указатель pFile станет нулевым.
Немного другой вид функция приобретает при передаче параметра-ссылки:
OnRightClick(File &file) { try {
TextFile pFile = dynamic_cast<TextFile &>(file); , // обработка текстового файла и HTML-файла
}
catch(std::bad_cast &noTextFile) { // обработка двоичного файла }
}
Если преобразование ссылки обошлось без генерации исключения, значит, имеем в наличии текстовый файл. Если же ссылку преобразовать не удалось, то файл, очевидно, не является текстовым — возникает исключение, и мы его перехватываем. В секции-ловушке выполняется обработка двоичного файла.
Перекрестные ссылки можно делать, например, так:
struct А { double d:
virtual ~А(){}; // чтобы сделать класс полиморфным
>:
struct В { double г: bool b:
};
struct D: public A, public B { int k;
D() { b=true: d=r=0.0; k=l; }
};
A *pa = new D();
B *pb = dynamic_cast<B*>(pa);
Статический тип pa — указатель на А, динамический — указатель на D. Обычный static_cast<> не может преобразовать указатель на А в указатель на В, так как классы А и В независимы. Можно использовать оператор reinterpret_cast<> или преобразование в стиле С (В*) — трансляция пройдет без ошибок, однако результаты работы будут неправильными. В этом легко убедиться, если выполнить простой оператор вывода:
cout << pb->b << endl:
Так как поле b в конструкторе D() инициализируется как true, на экране должна появиться цифра 1. Однако при использовании оператора reinterpret_cast<> выводится нуль. В то же время при использовании оператора dynamic_cast<> все работает правильно. В этой проблеме легко разобраться, если проверить значения указателей в том и другом случаях: при использовании оператора reinterpret_cast<> адреса, содержащиеся в ра и pb, одинаковы, а при использовании оператора dynamic_cast<> — разные. Это объясняется тем, что в объекте типа D объекты типа А и В занимают разные места. Оператор dynamic_ cast<> правильно вычисляет адрес подобъекта типа В внутри D, а оператор reinterpret_cast<> этого не делает.
Понижающее приведение с помощью оператора dynamic_cast<> выполняется обычно, чтобы преобразовать указатель на виртуальную базу в указатель на один из дочерних классов, например:
struct V
{ virtual ~V() {}: // полиморфный класс - виртуальная база
}:
struct A: virtual V {}; struct В: virtual V {}; struct D: public A. public В {}; V *pv = new D;
A *pa = dynamic__cast<A*>(pv); // понижающее приведение
Программистам приходится часто налаживать сеть. Для этого нужны кабели. Сэкономить можно тут купить кабель оптом .
Указатель pv имеет статический тип V, а указатель ра — статический тип А; преобразование выполняется от базы к наследнику. Это возможно вследствие того, что динамический тип указателя pv — тип D, а значит, в динамике выполняется повышающее приведение от D к А. |