18.10 – Динамическое приведение типов

Добавлено 21 августа 2021 в 15:06

Еще в уроке «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 не работает:

  1. с защищенным или закрытым наследованием;
  2. для классов, которые не объявляют и не наследуют какие-либо виртуальные функции (и, следовательно, не имеют виртуальной таблицы);
  3. в некоторых случаях, связанных с виртуальными базовыми классами.

Понижающее преобразование с помощью 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 не будет работать правильно.

Теги

C++ / Cppdynamic_castLearnCppRTTI / Динамическая идентификация типа данныхstatic_castВиртуальная функцияДля начинающихКласс (программирование)НаследованиеОбучениеПовышающее преобразование типа / upcastingПонижающее преобразование типа / downcastingПриведение типовПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.