Идентификация типов во время выполнения
RTTI позволяет программам, которые манипулируют объектами через указатели или ссылки на базовые классы, получить истинный производный тип адресуемого объекта. Для поддержки RTTI в языке C++ есть два оператора:
· оператор dynamic_cast поддерживает преобразования типов во время выполнения, обеспечивая безопасную навигацию по иерархии классов. Он позволяет трансформировать указатель на базовый класс в указатель на производный от него, а также преобразовать l-значение, ссылающееся на базовый класс, в ссылку на производный, но только в том случае, если это завершится успешно;
· оператор typeid позволяет получить фактический производный тип объекта, адресованного указателем или ссылкой.
Однако для получения информации о типе производного класса операнд любого из операторов dynamic_cast или typeid должен иметь тип класса, в котором есть хотя бы одна виртуальная функция. Таким образом, операторы RTTI – это события времени выполнения для классов с виртуальными функциями и события времени компиляции для всех остальных типов. В данном разделе мы более подробно познакомимся с их возможностями.
Использование RTTI оказывается необходимым при реализации таких приложений, как отладчики или объектные базы данных, когда тип объектов, которыми манипулирует программа, становится известен только во время выполнения путем исследования RTTI-информации, хранящейся вместе с типами объектов. Однако лучше пользоваться статической системой типов C++, поскольку она безопаснее и эффективнее.
Оператор dynamic_cast
Оператор dynamic_cast можно применять для преобразования указателя, ссылающегося на объект типа класса в указатель на тип класса из той же иерархии. Его также используют для трансформации l-значения объекта типа класса в ссылку на тип класса из той же иерархии. Приведение типов с помощью оператора dynamic_cast, в отличие от других имеющихся в C++ способов, осуществляется во время выполнения программы. Если указатель или l-значение не могут быть преобразованы в целевой тип, то dynamic_cast завершается неудачно. В случае приведения типа указателя признаком неудачи служит возврат нулевого значения. Если же l-значение нельзя трансформировать в ссылочный тип, возбуждается исключение. Ниже мы приведем примеры неудачного выполнения этого оператора.
Прежде чем перейти к более детальному рассмотрению dynamic_cast, посмотрим, зачем его нужно применять. Предположим, что в программе используется библиотека классов для представления различных категорий служащих компании. Входящие в иерархию классы поддерживают функции-члены для вычисления зарплаты:
class employee {
public:
virtual int salary();
};
class manager : public employee {
public:
int salary();
};
class programmer : public employee {
public:
int salary();
};
void company::payroll( employee *pe ) {
// используется pe->salary()
| }
В компании есть разные категории служащих. Параметром функции-члена payroll() класса company является указатель на объект employee, который может адресовать один из типов manager или programmer. Поскольку payroll() обращается к виртуальной функции-члену salary(), то вызывается подходящая замещающая функция, определенная в классе manager или programmer, в зависимости от того, какой объект адресован указателем.
Допустим, класс employee перестал удовлетворять нашим потребностям, и мы хотим его модифицировать, добавив еще одну функцию-член bonus(), используемую совместно с salary() при расчете платежной ведомости. Для этого нужно включить новую функцию-член в классы, составляющие иерархию employee:
class employee {
public:
virtual int salary(); // çàðïëàòà
virtual int bonus(); // ïðåìèÿ
};
class manager : public employee {
public:
int salary();
};
class programmer : public employee {
public:
int salary();
int bonus();
};
void company::payroll( employee *pe ) {
// èñïîëüçóåòñÿ pe->salary() è pe->bonus()
| }
Если параметр pe функции payroll() указывает на объект типа manager, то вызывается виртуальная функция-член bonus() из базового класса employee, поскольку в классе manager она не замещена. Если же pe указывает на объект типа programmer, то вызывается виртуальная функция-член bonus() из класса programmer.
После добавления новых виртуальных функций в иерархию классов придется перекомпилировать все функции-члены. Добавить bonus() можно, если у нас есть доступ к исходным текстам функций-членов в классах employee, manager и programmer. Однако если иерархия была получена от независимого поставщика, то не исключено, что в нашем распоряжении имеются только заголовочные файлы, описывающие интерфейс библиотечных классов и объектные файлы с их реализацией, а исходные тексты функций-членов недоступны. В таком случае перекомпиляция всей иерархии невозможна.
Если мы хотим расширить функциональность библиотеки классов, не добавляя новые виртуальные функции-члены, можно воспользоваться оператором dynamic_cast.
Этот оператор применяется для получения указателя на производный класс, чтобы иметь возможность работать с теми его элементами, которые по-другому не доступны. Предположим, что мы расширяем библиотеку за счет добавления новой функции-члена bonus() в класс programmer. Ее объявление можно включить в определение programmer, находящееся в заголовочном файле, а саму функцию определить в одном из своих исходных файлов:
class employee {
public:
virtual int salary();
};
class manager : public employee {
public:
int salary();
};
class programmer : public employee {
public:
int salary();
int bonus();
| };
Напомним, что payroll() принимает в качестве параметра указатель на базовый класс employee. Мы можем применить оператор dynamic_cast для получения указателя на производный programmer и воспользоваться им для вызова функции-члена bonus():
void company::payroll( employee *pe )
{
programmer *pm = dynamic_cast< programmer* >( pe );
// åñëè pe óêàçûâàåò íà îáúåêò òèïà programmer,
// òî dynamic_cast âûïîëíèòñÿ óñïåøíî è pm áóäåò
// óêàçûâàòü íà íà÷àëî îáúåêòà programmer
if ( pm ) {
// èñïîëüçîâàòü pm äëÿ âûçîâà programmer::bonus()
}
// åñëè pe íå óêàçûâàåò íà îáúåêò òèïà programmer,
// òî dynamic_cast âûïîëíèòñÿ íåóäà÷íî
// è pm áóäåò ñîäåðæàòü 0
else {
// èñïîëüçîâàòü ôóíêöèè-÷ëåíû êëàññà employee
}
| }
Оператор
dynamic_cast< programmer* >( pe )
приводит свой операнд pe к типу programmer*. Преобразование будет успешным, если pe ссылается на объект типа programmer, и неудачным в противном случае: тогда результатом dynamic_cast будет 0.
Таким образом, оператор dynamic_cast осуществляет сразу две операции. Он проверяет, выполнимо ли запрошенное приведение, и если это так, выполняет его. Проверка производится во время работы программы. dynamic_cast безопаснее, чем другие операции приведения типов в C++, поскольку проверяет возможность корректного преобразования.
Если в предыдущем примере pe действительно указывает на объект типа programmer, то операция dynamic_cast завершится успешно и pm будет инициализирован указателем на объект типа programmer. В противном случае pm получит значение 0. Проверив значение pm, функция company::payroll() может узнать, указывает ли pm на объект programmer. Если это так, то она вызывает функцию-член programmer::bonus() для вычисления премии программисту. Если же dynamic_cast завершается неудачно, то pe указывает на объект типа manager, а значит, необходимо применить более общий алгоритм расчета, не использующий новую функцию-член programmer::bonus().
Оператор dynamic_cast употребляется для безопасного приведения указателя на базовый класс к указателю на производный. Такую операцию часто называют понижающим приведением (downcasting). Она применяется, когда необходимо воспользоваться особенностями производного класса, отсутствующими в базовом. Манипулирование объектами производного класса с помощью указателей на базовый обычно происходит автоматически, с помощью виртуальных функций. Однако иногда использовать виртуальные функции невозможно. В таких ситуациях dynamic_cast предлагает альтернативное решение, хотя этот механизм в большей степени подвержен ошибкам, чем виртуализация, и должен применяться с осторожностью.
Одна из возможных ошибок – это работа с результатом dynamic_cast без предварительной проверки на 0: нулевой указатель нельзя использовать для адресации объекта класса. Например:
void company::payroll( employee *pe )
{
programmer *pm = dynamic_cast< programmer* >( pe );
// ïîòåíöèàëüíàÿ îøèáêà: pm èñïîëüçóåòñÿ áåç ïðîâåðêè çíà÷åíèÿ
static int variablePay = 0;
variablePay += pm->bonus();
// ...
| }
Результат, возвращенный dynamic_cast, всегда следует проверять, прежде чем использовать в качестве указателя. Более правильное определение функции company::payroll() могло бы выглядеть так:
void company::payroll( employee *pe )
{
// âûïîëíèòü dynamic_cast è ïðîâåðèòü ðåçóëüòàò
if ( programmer *pm = dynamic_cast< programmer* >( pe ) ) {
// èñïîëüçîâàòü pm äëÿ âûçîâà programmer::bonus()
}
else {
// èñïîëüçîâàòü ôóíêöèè-÷ëåíû êëàññà employee
}
| }
Результат операции dynamic_cast используется для инициализации переменной pm внутри условного выражения в инструкции if. Это возможно, так как объявления в условиях возвращают значения. Ветвь, соответствующая истинности условия, выполняется, если pm не равно нулю: мы знаем, что операция dynamic_cast завершилась успешно и pe указывает на объект programmer. В противном случае результатом объявления будет 0 и выполняется ветвь else. Поскольку теперь оператор и проверка его результата находятся в одной инструкции программы, то невозможно случайно вставить какой-либо код между выполнением dynamic_cast и проверкой, так что pm будет использоваться только тогда, когда содержит правильный указатель.
В предыдущем примере операция dynamic_cast преобразует указатель на базовый класс в указатель на производный. Ее также можно применять для трансформации l-значения типа базового класса в ссылку на тип производного. Синтаксис такого использования dynamic_cast следующий:
dynamic_cast< Type & >( lval )
где Type& – это целевой тип преобразования, а lval – l-значение типа базового класса. Операнд lval успешно приводится к типу Type& только в том случае, когда lval действительно относится к объекту класса, для которого один из производных имеет тип Type.
Поскольку нулевых ссылок не бывает (см. раздел 3.6), то проверить успешность выполнения операции путем сравнения результата (т.е. возвращенной оператором dynamic_cast ссылки) с нулем невозможно. Если вместо указателей используются ссылки, условие
if ( programmer *pm = dynamic_cast< programmer* >( pe ) )
нельзя переписать в виде
if ( programmer &pm = dynamic_cast< programmer& >( pe ) )
Для извещения об ошибке в случае приведения к ссылочному типу оператор dynamic_cast возбуждает исключение. Следовательно, предыдущий пример можно записать так:
#include <typeinfo>
void company::payroll( employee &re )
{
try {
programmer &rm = dynamic_cast< programmer & >( re );
// èñïîëüçîâàòü rm äëÿ âûçîâà programmer::bonus()
}
catch ( std::bad_cast ) {
// èñïîëüçîâàòü ôóíêöèè-÷ëåíû êëàññà employee
}
| }
В случае неудачного завершения ссылочного варианта dynamic_cast возбуждается исключение типа bad_cast. Класс bad_cast определен в стандартной библиотеке; для ссылки на него необходимо включить в программу заголовочный файл <typeinfo>. (Исключения из стандартной библиотеки мы будем рассматривать в следующем разделе.)
Когда следует употреблять ссылочный вариант dynamic_cast вместо указательного? Это зависит только от желания программиста. При его использовании игнорировать ошибку приведения типа и работать с результатом без проверки (как в указательном варианте) невозможно; с другой стороны, применение исключений увеличивает накладные расходы во время выполнения программы (см. главу 11).
Оператор typeid
Второй оператор, входящий в состав RTTI, – это typeid, который позволяет выяснить фактический тип выражения. Если оно принадлежит типу класса и этот класс содержит хотя бы одну виртуальную функцию-член, то ответ может и не совпадать с типом самого выражения. Так, если выражение является ссылкой на базовый класс, то typeid сообщает тип производного класса объекта:
#include <typeinfo>
programmer pobj;
employee &re = pobj;
// ñ ôóíêöèåé name() ìû ïîçíàêîìèìñÿ â ïîäðàçäåëå, ïîñâÿùåííîì type_info
// îíà âîçâðàùàåò C-ñòðîêó "programmer"
| coiut << typeid( re ).name() << endl;
Операнд re оператора typeid имеет тип employee. Но так как re – это ссылка на тип класса с виртуальными функциями, то typeid говорит, что тип адресуемого объекта – programmer (а не employee, на который ссылается re). Программа, использующая такой оператор, должна включать заголовочный файл <typeinfo>, что мы и сделали в этом примере.
Где применяется typeid? В сложных системах разработки, например при построении отладчиков, а также при использовании устойчивых объектов, извлеченных из базы данных. В таких системах необходимо знать фактический тип объекта, которым программа манипулирует с помощью указателя или ссылки на базовый класс, например для получения списка его свойств во время сеанса работы с отладчиком или для правильного сохранения или извлечения объекта из базы данных. Оператор typeid допустимо использовать с выражениями и именами любых типов. Например, его операндами могут быть выражения встроенных типов и константы. Если операнд не принадлежит к типу класса, то typeid просто возвращает его тип:
int iobj;
cout << typeid( iobj ).name() << endl; // ïå÷àòàåòñÿ: int
| cout << typeid( 8.16 ).name() << endl; // печатается: double
Если операнд имеет тип класса, в котором нет виртуальных функций, то typeid возвращает тип операнда, а не связанного с ним объекта:
class Base { /* нет виртуальных функций */ };
class Derived : public Base { /* íåò âèðòóàëüíûõ ôóíêöèé */ };
Derived dobj;
Base *pb = &dobj;
| cout << typeid( *pb ).name() << endl; // печатается: Base
Операнд typeid имеет тип Base, т.е. тип выражения *pb. Поскольку в классе Base нет виртуальных функций, результатом typeid будет Base, хотя объект, на который указывает pb, имеет тип Derived.
Результаты, возвращенные оператором typeid, можно сравнивать. Например:
#include <typeinfo>
employee *pe = new manager;
employee& re = *pe;
if ( typeid( pe ) == typeid( employee* ) ) // èñòèííî
// ÷òî-òî ñäåëàòü
/*
if ( typeid( pe ) == typeid( manager* ) ) // ëîæíî
if ( typeid( pe ) == typeid( employee ) ) // ëîæíî
if ( typeid( pe ) == typeid( manager ) ) // ëîæíî
| */
Условие в инструкции if сравнивает результаты применения typeid к операнду, являющемуся выражением, и к операнду, являющемуся именем типа. Обратите внимание, что сравнение
typeid( pe ) == typeid( employee* )
возвращает истину. Это удивит пользователей, привыкших писать:
// вызов виртуальной функции
| pe->salary();
что приводит к вызову виртуальной функции salary() из производного класса manager. Поведение typeid(pe) не подчиняется данному механизму. Это связано с тем, что pe – указатель, а для получения типа производного класса операндом typeid должен быть тип класса с виртуальными функциями. Выражение typeid(pe) возвращает тип pe, т.е. указатель на employee. Это значение совпадает со значением typeid(employee*), тогда как все остальные сравнения дают ложь.
Только при употреблении выражения *pe в качестве операнда typeid результат будет содержать тип объекта, на который указывает pe:
typeid( *pe ) == typeid( manager ) // истинно
| typeid( *pe ) == typeid( employee ) // ложно
В этих сравнениях *pe – выражение типа класса, который имеет виртуальные функции, поэтому результатом применения typeid будет тип адресуемого операндом объекта manager.
Такой оператор можно использовать и со ссылками:
typeid( re ) == typeid( manager ) // истинно
typeid( re ) == typeid( employee ) // ложно
typeid( &re ) == typeid( employee* ) // истинно
| typeid( &re ) == typeid( manager* ) // ложно
В первых двух сравнениях операнд re имеет тип класса с виртуальными функциями, поэтому результат применения typeid содержит тип объекта, на который ссылается re. В последних двух сравнениях операнд &re имеет тип указателя, следовательно, результатом будет тип самого операнда, т.е. employee*.
На самом деле оператор typeid возвращает объект класса типа type_info, который определен в заголовочном файле <typeinfo>. Интерфейс этого класса показывает, что можно делать с результатом, возвращенным typeid. (В следующем подразделе мы подробно рассмотрим этот интерфейс.)
Класс type_info
Точное определение класса type_info зависит от реализации, но некоторые его характерные черты остаются неизменными в любой программе на C++:
class type_info {
// ïðåäñòàâëåíèå çàâèñèò îò ðåàëèçàöèè
private:
type_info( const type_info& );
type_info& operator= ( const type_info& );
public:
virtual ~type_info();
int operator==( const type_info& );
int operator!=( const type_info& );
const char * name() const;
| };
Поскольку копирующие конструктор и оператор присваивания – закрытые члены класса type_info, то пользователь не может создать его объекты в своей программе:
#include <typeinfo>
type_info t1; // îøèáêà: íåò êîíñòðóêòîðà ïî óìîë÷àíèþ
// îøèáêà: êîïèðóþùèé êîíñòðóêòîð çàêðûò
| type_info t2 (typeid( unsigned int ) );
Единственный способ создать объект класса type_info – воспользоваться оператором typeid.
В классе определены также операторы сравнения. Они позволяют сравнивать два объекта type_info, а следовательно, и результаты, возвращенные двумя операторами typeid. (Мы говорили об этом в предыдущем подразделе.)
typeid( re ) == typeid( manager ) // èñòèííî
| typeid( *pe ) != typeid( employee ) // ложно
Функция name() возвращает C-строку с именем типа, представленного объектом type_info. Этой функцией можно пользоваться в программах следующим образом:
#include <typeinfo>
int main() {
employee *pe = new manager;
// ïå÷àòàåò: "manager"
cout << typeid( *pe ).name() << endl;
| }
Для работы с функцией-членом name() нужно включить заголовочный файл <typeinfo>.
Имя типа – это единственная информация, которая гарантированно возвращается всеми реализациями C++, при этом используется функция-член name() класса type_info. В начале этого раздела упоминалось, что поддержка RTTI зависит от реализации и иногда в классе type_info бывают дополнительные функции-члены. Чтобы узнать, каким образом обеспечивается поддержка RTTI в вашем компиляторе, обратитесь к справочному руководству по нему. Кроме того, можно получить любую информацию, которую компилятор знает о типе, например:
· список функций-членов класса;
· способ размещения объекта в памяти, т.е. взаимное расположение подобъектов базового и производных классов.
Одним из способов расширения поддержки RTTI является включение дополнительной информации в класс, производный от type_info. Поскольку в классе type_info есть виртуальный деструктор, то оператор dynamic_cast позволяет выяснить, имеется ли некоторое конкретное расширение RTTI. Предположим, что некоторый компилятор предоставляет расширенную поддержку RTTI посредством класса extended_type_info, производного от type_info. С помощью оператора dynamic_cast программа может узнать, принадлежит ли объект типа type_info, возвращенный оператором typeid, к типу extended_type_info. Если да, то пользоваться расширенной поддержкой RTTI разрешено.
#include <typeinfo>
// Файл typeinfo содержит определение типа extended_type_info
void func( employee* p )
{
// понижающее приведение типа type_info* к extended_type_info*
if ( eti *eti_p = dynamic_cast<eti *>( &typeid( *p ) ) )
{
// если dynamic_cast завершается успешно,
// можно пользоваться информацией из extended_type_info через eti_p
}
else
{
// если dynamic_cast завершается неудачно,
// можно пользоваться только стандартным type_info
}
| }
Если dynamic_cast завершается успешно, то оператор typeid вернет объект класса extended_type_info, т.е. компилятор обеспечивает расширенную поддержку RTTI, чем программа может воспользоваться. В противном случае допустимы только базовые средства RTTI.
Упражнение 19.1
Дана иерархия классов, в которой у каждого класса есть конструктор по умолчанию и виртуальный деструктор:
class X { ... };
class A { ... };
class B : public A { ... };
class C : public B { ... };
| class D : public X, public C { ... };
Какие из данных операторов dynamic_cast завершатся неудачно?
A *pa = dynamic_cast< A* > ( pd );
C *pc = dynamic_cast< C* > ( pa );
D *pd = dynamic_cast< D* > ( pb );
X *px = dynamic_cast< X* > ( pa );
Упражнение 19.2
Объясните, когда нужно пользоваться оператором dynamic_cast вместо виртуальной функции?
Упражнение 19.3
Пользуясь иерархией классов из упражнения 19.1, перепишите следующий фрагмент так, чтобы в нем использовался ссылочный вариант dynamic_cast для преобразования *pa в тип D&:
if ( D *pd = dynamic_cast< D* >( pa ) ) {
// использовать члены D
}
else {
// использовать члены A
| }
Упражнение 19.4
Дана иерархия классов, в которой у каждого класса есть конструктор по умолчанию и виртуальный деструктор:
class X { ... };
class A { ... };
class B : public A { ... };
class C : public B { ... };
| class D : public X, public C { ... };
Какое имя типа будет напечатано в каждом из следующих случаев:
(a) A *pa = new D;
cout << typeid( pa ).name() << endl;
(b) X *px = new D;
cout << typeid( *px ).name() << endl;
(c) C obj;
A& ra = cobj;
cout << typeid( &ra ).name() << endl;
(d) X *px = new D;
A& ra = *px;
| cout << typeid( ra ).name() << endl;
Не нашли, что искали? Воспользуйтесь поиском по сайту:
©2015 - 2024 stydopedia.ru Все материалы защищены законодательством РФ.
|