Динамическое преобразование

Последнее обновление: 16.03.2023

Динамическое приведение типов, в отличие от статического, выполняется во время выполнения программы. Для этого применяется функция dynamic_cast<>(). Также как и для static_cast, в угловых скобках указывается тип, к которому выполняется преобразование, в круглые скобки передается преобразуемый объект:

dynamic_cast<тип_в_который_преобразуем>(преобразуемый_объект)

Но эту функцию можно применять только к указателям и ссылкам на полиморфные типы классов, которые содержат хотя бы одну виртуальную функцию. Причина в том, что только указатели на типы полиморфных классов содержат информацию, которая необходима функции dynamic_cast для проверки правильности преобразования. Конечно, типы, между которыми выполняется преобразование, должны быть указателями или ссылками на классы в одной иерархии классов.

Есть два вида динамического приведения. Первый — это преобразование от указателя на базовый класс к указателю на производный класс - так называемое нисхолящее преобразование или downcast (базовые классы в иерархии помещаются вверху, а производные внизу, поэтому преобразование идет сверху вниз). Второй тип — преобразование между базовыми типами в одной иерархии (при множественном наследовании) - кросскаст (crosscast).

Рассмотрим следующую программу:

#include <iostream>

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;
}

Преобразование smart-указателей

Для динамического преобразования смарт-указателей 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.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850