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
не будет работать правильно.