Динамическое приведение типов, в отличие от статического, выполняется во время выполнения программы. Для этого применяется функция dynamic_cast<>().
Также как и для static_cast, в угловых скобках указывается тип, к которому выполняется преобразование, в круглые скобки передается преобразуемый объект:
dynamic_cast<тип_в_который_преобразуем>(преобразуемый_объект)
Но эту функцию можно применять только к указателям и ссылкам на полиморфные типы классов, которые содержат хотя бы одну виртуальную функцию.
Причина в том, что только указатели на типы полиморфных классов содержат информацию, которая необходима функции dynamic_cast
для проверки правильности преобразования. Конечно, типы, между которыми выполняется преобразование, должны быть указателями или ссылками на классы в одной иерархии классов.
Есть два вида динамического приведения. Первый — это преобразование от указателя на базовый класс к указателю на производный класс - так называемое нисхолящее преобразование или downcast (базовые классы в иерархии помещаются вверху, а производные внизу, поэтому преобразование идет сверху вниз). Второй тип — преобразование между базовыми типами в одной иерархии (при множественном наследовании) - кросскаст (crosscast).
Рассмотрим следующую программу:
#include <iostream>
#include <string>
class Book // класс книги
{
public:
Book(std::string title, unsigned pages): title{title}, pages{pages}{}
std::string getTitle() const {return title;}
unsigned getPages() const {return pages;}
virtual void print() const
{
std::cout << title << ". Pages: " << pages << std::endl;
}
private:
std::string title; // название книги
unsigned pages; // количество страниц
};
class File // класс электронного файла
{
public:
File(unsigned size): size{size}{}
unsigned getSize() const {return size;}
virtual void print() const
{
std::cout << "Size: " << size << std::endl;
}
private:
unsigned size; // размер в мегабайтах
};
class Ebook : public Book, public File // класс электронной книги
{
public:
Ebook(std::string title, unsigned pages, unsigned size): Book{title, pages}, File{size}{}
void print() const override
{
std::cout << getTitle() << "\tPages: " << getPages() << "\tSize: " << getSize() << "Mb" << std::endl;
}
};
int main()
{
Ebook cppbook{"About C++", 350, 6};
Book* book = &cppbook; // указывает на объект Ebook
// динамическое преобразование из Book в Ebook
Ebook* ebook{dynamic_cast<Ebook*>(book)};
ebook->print(); // About C++ Pages: 350 Size: 6Mb
}
Здесь у нас есть класс Book, который представляет книгу с переменными title и pages для хранения названия книги и количества страниц. А также есть класс File, который представляет электронный файл, в котором для хранения размера определено поле size. Класс электронной книги Ebook наследуется от обоих классов.
Чтобы динамическое преобразование было возможно, базовые классы определяют виртуальную функцию print.
В функции main создаем один объект типа Ebook и его адрес передаем указателю book, который представляет базовый тип Book*. Поскольку этот указатель все таки хранит адрес объекта Ebook, то мы можем привести его к указателю на Ebook:
Ebook* ebook{dynamic_cast<Ebook*>(book)};
ebook->print(); // About C++ Pages: 350 Size: 6Mb
Далее через указатель мы сможем обращаться к функционалу класса Ebook.
Стоит отметить, что в данном случае динамическое преобразование не имеет смысла: мы могли бы вызвать у указателя book функцию print и за счет виртуальности функции получили бы тот же самый результат. Преобразование нужно, если нам необходимо обратиться к каким-то членам производного класса, которые не определены в базовом. Например, класс Book не имеет функции getSize(), и чтобы обратиться к ней могло потребоваться преобразование.
В примере выше был приведен так называемый downcast. Теперь рассмотрим crosscast:
int main()
{
Ebook cppbook{"About C++", 350, 6};
Book* book = &cppbook; // указывает на объект Ebook
// динамическое преобразование из Book в File - crosscast
File* file{dynamic_cast<File*>(book)};
file->print(); // About C++ Pages: 350 Size: 6Mb
}
Преобразование из указателя на Book в указатель на File является кросскастом и в данном случае возможно, потому что указатель book хранит адрес объекта Ebook, который также наследуется от File.
Но подобные преобразования не всегда выполняются успешно. В этом случае функция dynamic_cast() возвращает указатель nullptr, и после получения результата
мы можем проверить на это значение:
int main()
{
Book cppbook{"About C++", 350};
Book* book = &cppbook; // указывает на объект Book
// динамическое преобразование из Book в File - crosscast
File* file{dynamic_cast<File*>(book)};
// проверяем результат
if(file) // если file !=nullptr
{
file->print();
}
else
{
std::cout << "The book is not a file" << std::endl;
}
}
В данном случае указатель book хранит адрес объекта Book и поэтому не может быть преобразован к типу указателя на File. Поэтому вызов dynamic_cast<File*>(book)
возвратит nullptr. После этого мы можем проверить результат и в зависимости от результата проверки выполнить опреленные действия.
Обратите внимание, что если преобразуемый указатель является указателем на константу, то тип указателя, к которому выполняется приведение, также должен представлять указатель на константу:
int main()
{
const Ebook cppbook{"About C++", 350, 6};
const Book* book = &cppbook; // указатель на константу
// преобразование в указатель на константу
const Ebook* file{dynamic_cast<const Ebook*>(book)};
file->print();
}
Если необходимо выполнить приведение из указателя на константу в обычный указатель (не на константу), то сначала надо выполнить приведение к указателю того же типа, что и исходный, с помощью функции const_cast<T>():
int main()
{
const Ebook cppbook{"About C++", 350, 6};
const Book* const_book = &cppbook; // указатель на константу
// преобразование из указателя на константу в обычный указатель того же типа
Book* book {const_cast<Book*>(const_book)};
// преобразуем указатели
Ebook* file{dynamic_cast<Ebook*>(book)};
file->print();
}
Функция dynamic_cast также может применяться к ссылкам (из ссылки на базовый тип в ссылку на производный тип):
int main()
{
Ebook cppbook{"About C++", 350, 6};
Book& book {cppbook}; // ссылка на Ebook
// преобразуем в ссылку на Ebook
Ebook& file{dynamic_cast<Ebook&>(book)};
file.print();
}
В данном случае ссылка book в реальности ссылается на объект Ebook, поэтому эту ссылку можно преобразовать в ссылку Ebook&. Но что, если ссылка book не ссылается на объект Ebook:
int main()
{
Book cppbook{"About C++", 350};
Book& book {cppbook}; // ссылка на Book
// преобразуем в ссылку на Ebook
Ebook& file{dynamic_cast<Ebook&>(book)}; // ! Ошибка std::bad_cast
file.print();
}
В этом случае при преобразовании мы столкнемся с ошибкой, и программа завершит свое выполнение. Если с указателями мы могли бы проверить результат на nullptr, то в случае с ссылками мы этого сделать не можем. Однако, чтобы избежать некорректного преобразования ссылок мы опять же можем преобразовывать в соответствующий тип указателя
int main()
{
Book cppbook{"About C++", 350};
Book& book {cppbook}; // ссылка на Book
// преобразуем в ссылку на указатель на Ebook
Ebook* file{dynamic_cast<Ebook*>(&book)};
if(file)
file->print();
else
std::cout << "Object is not a file" << std::endl;
}
Для динамического преобразования смарт-указателей std::shared_ptr применяется функция std::dynamic_pointer_cast<T>():
int main()
{
// указатель std::shared_ptr<Book> указывает на объект Ebook
std::shared_ptr<Book> book{std::make_shared<Ebook>("About C++", 350, 6)};
// динамическое преобразование из Book в Ebook
std::shared_ptr<Ebook> ebook{std::dynamic_pointer_cast<Ebook>(book)};
ebook->print(); // About C++ Pages: 350 Size: 6Mb
}
В данном случае указатель book, который представляет тип std::shared_ptr<Book> в реальности указывает на объект Ebook. Поэтому его можно привести к типу
указателя std::shared_ptr<Ebook>. Если же преобразование невозможно, то функция возвращает nullptr:
int main()
{
// указатель std::shared_ptr<Book> указывает на объект Ebook
std::shared_ptr<Book> book{std::make_shared<Book>("About Java", 280)};
// динамическое преобразование из Book в Ebook
std::shared_ptr<Ebook> ebook{std::dynamic_pointer_cast<Ebook>(book)};
if(ebook) // if (ebook != nullptr)
{
ebook->print();
}
else
{
std::cout << "Object is not e-book" << std::endl;
}
}
В данном случае указатель book указывает на объект Book. Поэтому при преобразовании в указатель на объект Ebook функция возвратит nullptr.