Оператор 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).