18.10 – Динамическое приведение типов
Еще в уроке «8.5 – Явное преобразование (приведение) типов данных и static_cast» мы изучили концепцию преобразования типов и использование static_cast для преобразования переменных из одного типа в другой.
В этом уроке мы продолжим рассмотрение другого типа приведения: dynamic_cast.
Необходимость в dynamic_cast
Имея дело с полиморфизмом, вы часто сталкиваетесь со случаями, когда у вас есть указатель на базовый класс, но вы хотите получить доступ к какой-либо информации, которая существует только в производном классе.
Рассмотрим следующую (слегка надуманную) программу:
#include <iostream>
#include <string>
class Base
{
protected:
int m_value{};
public:
Base(int value)
: m_value{value}
{
}
virtual ~Base() = default;
};
class Derived : public Base
{
protected:
std::string m_name{};
public:
Derived(int value, const std::string& name)
: Base{value}, m_name{name}
{
}
const std::string& getName() const { return m_name; }
};
Base* getObject(bool returnDerived)
{
if (returnDerived)
return new Derived{1, "Apple"};
else
return new Base{2};
}
int main()
{
Base* b{ getObject(true) };
// как напечатать здесь имя объекта Derived, имея только указатель Base?
delete b;
return 0;
}
В этой программе функция getObject() всегда возвращает указатель Base, но этот указатель может указывать либо на объект Base, либо на объект Derived. В случае, когда указатель указывает на объект Derived, как нам вызвать Derived::getName()?
Один из способов – добавить в Base виртуальную функцию с именем getName() (чтобы мы могли вызывать ее с помощью указателя/ссылки Base и динамически разрешать ее вызов в Derived::getName()). Но что бы эта функция вернула, если бы вы вызвали ее с помощью указателя/ссылки Base, которая на самом деле указывала на объект Base? У него нет никакого осмысленного значения для возврата. Более того, мы загрязнили бы наш класс Base вещами, которые на самом деле должны быть заботой только класса Derived.
Мы знаем, что C++ позволяет вам неявно преобразовать указатель Derived в указатель Base (фактически, getObject() делает именно это). Этот процесс иногда называют повышающим преобразование (upcasting). Однако что, если бы существовал способ преобразовать указатель Base обратно в указатель Derived? Тогда мы могли бы вызвать Derived::getName() напрямую, используя этот указатель, и вообще не беспокоиться о добавлении виртуальной функции.
dynamic_cast
C++ предоставляет оператор приведения с именем dynamic_cast, который можно использовать только для этой цели. Хотя динамическое приведение имеет несколько различных возможностей, наиболее распространенное использование динамического приведения – преобразование указателей базового класса в указатели производного класса. Этот процесс называется понижающим преобразованием (downcasting).
Использование dynamic_cast работает так же, как static_cast. Вот наш пример main() выше, с использованием dynamic_cast для преобразования нашего указателя Base обратно в указатель Derived:
int main()
{
Base* b{ getObject(true) };
// использовать динамическое приведение для преобразования
// указателя Base в указатель Derived
Derived* d{ dynamic_cast<Derived*>(b) };
std::cout << "The name of the Derived is: " << d->getName() << '\n';
delete b;
return 0;
}
Этот код печатает:
The name of the Derived is: Apple
Сбой dynamic_cast
Приведенный выше пример работает, потому что b фактически указывает на объект Derived, поэтому преобразование b в указатель Derived проходит успешно.
Однако мы сделали довольно опасное предположение, что b указывает на объект Derived. Что, если бы b не указывал на объект Derived? Это легко проверить, изменив аргумент getObject() с true на false. В этом случае getObject() вернет указатель Base на объект Base. Когда мы попытаемся выполнить динамическое преобразование в объект Derived, это не удастся, потому что это преобразование не может быть выполнено.
Если dynamic_cast завершается неудачей, результатом преобразования будет нулевой указатель.
Поскольку мы не проверили результат на нулевой указатель, мы обращаемся к d->getName(), что пытается разыменовать нулевой указатель, что приводит к неопределенному поведению (возможно, к сбою).
Чтобы сделать эту программу безопасной, нам нужно убедиться, что результат выполнения dynamic_cast действительно успешен:
int main()
{
Base* b{ getObject(true) };
// использовать динамическое приведение для преобразования
// указателя Base в указатель Derived
Derived* d{ dynamic_cast<Derived*>(b) };
if (d) // убеждаемся, что d - не нулевой указатель
std::cout << "The name of the Derived is: " << d->getName() << '\n';
delete b;
return 0;
}
Правило
Всегда проверяйте результат выполнения динамического приведения, проверяя результат на нулевой указатель.
Обратите внимание: поскольку dynamic_cast во время выполнения выполняет какую-то проверку согласованности (чтобы гарантировать, что преобразование может быть выполнено), использование dynamic_cast приводит к снижению производительности.
Также обратите внимание, что есть несколько случаев, когда понижающее преобразование с использованием dynamic_cast не работает:
- с защищенным или закрытым наследованием;
- для классов, которые не объявляют и не наследуют какие-либо виртуальные функции (и, следовательно, не имеют виртуальной таблицы);
- в некоторых случаях, связанных с виртуальными базовыми классами.
Понижающее преобразование с помощью static_cast
Оказывается, что понижающее преобразование также можно выполнить с помощью static_cast. Основное отличие состоит в том, что static_cast не выполняет проверку типов во время выполнения, чтобы убедиться, что то, что вы делаете, имеет смысл. Это делает использование static_cast более быстрым, но более опасным. Если вы приведете Base* к Derived*, преобразование будет «успешным», даже если указатель Base не указывает на объект Derived. Это приведет к неопределенному поведению, когда вы попытаетесь получить доступ к полученному указателю Derived (который на самом деле указывает на объект Base).
Если вы абсолютно уверены, что указатель, для которого вы выполняете понижающее преобразование, будет корректным, использование static_cast допустимо. Один из способов убедиться, что вы знаете, на какой тип объекта вы указываете, – использовать виртуальную функцию. Вот один из способов (не самый лучший, потому что в нем используется глобальная переменная):
#include <iostream>
#include <string>
// Идентификатор класса
enum class ClassID
{
base,
derived
// другие могут быть добавлены сюда позже
};
class Base
{
protected:
int m_value{};
public:
Base(int value)
: m_value{value}
{
}
virtual ~Base() = default;
virtual ClassID getClassID() const { return ClassID::base; }
};
class Derived : public Base
{
protected:
std::string m_name{};
public:
Derived(int value, const std::string& name)
: Base{value}, m_name{name}
{
}
const std::string& getName() const { return m_name; }
virtual ClassID getClassID() const { return ClassID::derived; }
};
Base* getObject(bool bReturnDerived)
{
if (bReturnDerived)
return new Derived{1, "Apple"};
else
return new Base{2};
}
int main()
{
Base* b{ getObject(true) };
if (b->getClassID() == ClassID::derived)
{
// Мы уже доказали, что b указывает на объект Derived,
// поэтому это преобразование всегда должно быть успешным
Derived* d{ static_cast<Derived*>(b) };
std::cout << "The name of the Derived is: " << d->getName() << '\n';
}
delete b;
return 0;
}
Но если вы готовы пройти через все эти проблемы, чтобы реализовать это (и заплатить за вызов виртуальной функции и обработку результата), вы можете просто использовать dynamic_cast.
dynamic_cast и ссылки
Хотя во всех приведенных выше примерах показано динамическое приведение указателей (что является более распространенным), dynamic_cast также может использоваться и со ссылками. Это работает аналогично тому, как dynamic_cast работает с указателями.
#include <iostream>
#include <string>
class Base
{
protected:
int m_value;
public:
Base(int value)
: m_value{value}
{
}
virtual ~Base() = default;
};
class Derived : public Base
{
protected:
std::string m_name;
public:
Derived(int value, const std::string& name)
: Base{value}, m_name{name}
{
}
const std::string& getName() const { return m_name; }
};
int main()
{
// создаем an apple
Derived apple{1, "Apple"};
// устанавливаем ссылку базового класса на этот объект
Base& b{ apple };
// динамическое приведение с использованием ссылки вместо указателя
Derived& d{ dynamic_cast<Derived&>(b) };
// мы можем получить доступ к Derived::getName через d
std::cout << "The name of the Derived is: " << d.getName() << '\n';
return 0;
}
Поскольку C++ не имеет «нулевой ссылки», dynamic_cast не может возвращать нулевую ссылку в случае ошибки. Вместо этого, если dynamic_cast ссылки терпит неудачу, генерируется исключение типа std::bad_cast. Об исключениях мы поговорим позже в этой серии статей.
dynamic_cast против static_cast
Начинающие программисты иногда не понимают, когда использовать static_cast, а когда dynamic_cast. Ответ довольно прост: используйте static_cast, если только вы не выполняете понижающие преобразование, так как в этом случае dynamic_cast обычно лучше. Однако вам также следует подумать о том, чтобы полностью избежать приведения типов и просто использовать виртуальные функции.
Понижающее преобразование против виртуальных функций
Некоторые разработчики считают, что dynamic_cast – это зло и указывает на плохой дизайн класса. Эти программисты говорят, что вместо него вам следует использовать виртуальные функции.
В общем случае, использование виртуальной функции должно быть предпочтительнее понижающего преобразования. Однако бывают случаи, когда понижающее преобразование является лучшим выбором:
- когда вы не можете изменить базовый класс для добавления виртуальной функции (например, потому что базовый класс является частью стандартной библиотеки);
- когда вам нужен доступ к чему-то, специфичному для производного класса (например, функция доступа, которая существует только в производном классе);
- когда добавлять виртуальную функцию к базовому классу не имеет смысла (например, в базовом классе нет подходящего значения для возврата). Использование чисто виртуальной функции может быть здесь допустимо, если вам не нужно создавать экземпляры базового класса.
Предупреждение о dynamic_cast и RTTI
Динамическая идентификация типа данных (RTTI, run-time type information) – это функция C++, которая предоставляет информацию о типе данных объекта во время выполнения. Эта возможность используется в dynamic_cast. Поскольку RTTI требует значительных затрат производительности, некоторые компиляторы для оптимизации позволяют отключать RTTI. Излишне говорить, что если вы это сделаете, dynamic_cast не будет работать правильно.
