Архитектура Аудит Военная наука Иностранные языки Медицина Металлургия Метрология
Образование Политология Производство Психология Стандартизация Технологии


Идентификация типов во время выполнения



RTTI позволяет программам, которые манипулируют объектами через указатели или ссылки на базовые классы, получить истинный производный тип адресуемого объекта. Для поддержки RTTI в языке C++ есть два оператора:

· оператор dynamic_cast поддерживает преобразования типов во время выполнения, обеспечивая безопасную навигацию по иерархии классов. Он позволяет трансформировать указатель на базовый класс в указатель на производный от него, а также преобразовать l-значение, ссылающееся на базовый класс, в ссылку на производный, но только в том случае, если это завершится успешно;

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

Однако для получения информации о типе производного класса операнд любого из операторов dynamic_cast или typeid должен иметь тип класса, в котором есть хотя бы одна виртуальная функция. Таким образом, операторы RTTI – это события времени выполнения для классов с виртуальными функциями и события времени компиляции для всех остальных типов. В данном разделе мы более подробно познакомимся с их возможностями.

Использование RTTI оказывается необходимым при реализации таких приложений, как отладчики или объектные базы данных, когда тип объектов, которыми манипулирует программа, становится известен только во время выполнения путем исследования RTTI-информации, хранящейся вместе с типами объектов. Однако лучше пользоваться статической системой типов C++, поскольку она безопаснее и эффективнее.

19.1.1. Оператор 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. (В следующем подразделе мы подробно рассмотрим этот интерфейс.)

19.1.3. Класс 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) D *pd = new D;

A *pa = dynamic_cast< A* > ( pd );

(b) A *pa = new C;

C *pc = dynamic_cast< C* > ( pa );

(c) B *pb = new B;

D *pd = dynamic_cast< D* > ( pb );

(d) A *pa = new D;

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;


Поделиться:



Последнее изменение этой страницы: 2019-04-09; Просмотров: 290; Нарушение авторского права страницы


lektsia.com 2007 - 2024 год. Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав! (0.131 с.)
Главная | Случайная страница | Обратная связь