Язык программирования 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).


Содержание раздела