Приведение типов. Наглядное отличие static_cast от dynamic_cast

Доброго времени суток. Очень много статей в интернете о разнице операторов приведения типов, но понимания в данной теме они мне не особо то и не добавили. Пришлось разбираться самому. Хочу поделиться с вами моим опытом на довольно наглядном примере.

Статья рассчитана на тех, кто хочет осознать приведение типов в С++.

Итак, пусть у нас есть такая иерархия наследования:

#include <iostream>

struct A{
    A():a(0), b(0){}
  int a;
  int b;
};
struct B : A{
    B():g(0){}
    int g;
};
struct D{
    D():f(0){}
    float f;
};
struct C : A, D{
    C():d(0){}
    double d;
};


На картинке изображена иерархия наследования и расположение членов-данных наследников в памяти

image

Небольшое отступление: почему так важно преобразование типов? Говоря по рабоче-крестьянски, при присваивании объекту типа X объект типа Y, мы должны определить, какое значение будет иметь после присваивания объект типа X.

Начнем с использования static_cast:

int main(){
    C* pC = new C;
    A* pA = pC;
    D* pD = static_cast<D*> (pC); 
    std::cout << pС << " " << pD << " " << pA << std::endl;

    return 0;
}

Почему таков эффект при выводе значений указателей (значение указателя это адрес, по которому лежит переменная)? Дело в том, что static_cast производит сдвиг указателя.
Рассмотрим на примере:

D* pD = static_cast<D*> (pC); 

1. Происходит преобразование типа из C* в D*. Результатом этого есть указатель типа D* (назовем его tempD), который указывает (внимание!) на ту часть в объекте класса C, которая унаследована от класса D. Значение самого pC не меняется!

2. Теперь присваиваем указателю pD значение указателя tempD (всё хорошо, типы одинаковы)
Разумный вопрос: а зачем собственно нужно сдвигать указатель? Говоря по простому, указатель класса D* руководствуется определением класса D. Если бы не произошло смещения, то меняя значения переменных через указатель D, мы бы меняли переменные объекта класса С, которые не относятся к переменным, унаследованным от класса D (если бы указатель pD имел то же значение, что и pC, то при обращении pD->f в действительности мы бы работали с переменной
а).

Промежуточный итог: static_cast при работе с иерархией классов определяет значения указателей так, чтобы обращение к переменным класса через указатель было корректным.

Поговорим о недостатках static_cast. Вернемся к той же иерархии наследования.

Рассмотрим такой код:

int main(){
    C* pC = new C;
    A* pA = static_cast<A*>(pC);
    D* pD = static_cast<D*> (pC);
    B* pB = static_cast<B*> (pA);
    std::cout << &(pB->g) << " " << pD << " " << pA << std::endl;
    pB->g = 100;
    std::cout << pC->a << " " << pC->b << " " << pC->f << std::endl;
    return 0;
}

Почему pC->f имеет значение отличное от 0? Рассмотрим код по строчкам:

  1. В куче выделяется память под указатель типа С.
  2. Происходит повышающее преобразование. Указатель pA имеет такое же значение как и pC.
  3. Происходит повышающее преобразование. Указатель pD имеет значение, которое есть АДРЕС переменной f, в объекте класса C, на который указывает указатель pC.
  4. Происходит понижающее преобразование. Указатель pB имеет то же значение, что и указатель pA.

Где опасность? Дело в том, что в таком варианте исполнения, указатель pB действительно уверовал в то, что объект, на который указывал pA, был объектом типа B. При преобразовании static_cast проверяет, что такая иерархия действительно имеет место быть (т.е. что класс B является наследником класса A), но он не проверяет, что объект, на который указывает указатель pA, действительно является объектом типа B.

Сама опасность:

image

Теперь если мы хотим сделать запись в переменную g через указатель pB (ведь pB полностью уверен что указывает на объект типа B), мы на самом деле запишем данные в переменную f, унаследованную от класса D. Причем указатель pD будет интерпретировать информацию, записанную в переменную f, как float, что мы и видим при выводе через cout.

Как решить такую проблему?
Для этого следует использовать dynamic_cast, который проверяет не только валидность иерархии классов, но и тот факт, что указатель действительно указывает на объект того типа, к которому мы хотим привести.

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

Демонстрация решения проблемы, при той же иерархии классов:

int main(){
    C* pC = new C;
    A* pA = pC;
    if(D* pD = dynamic_cast<D*> (pC))
        std::cout << " OK " << std::endl;
    else
        std::cout << " not OK " << std::endl;
    if(B* pB = dynamic_cast<B*> (pA))
        std::cout << " OK " << std::endl;
    else
        std::cout << " not OK " << std::endl;
    return 0;
}

Предлагаю запустить код и убедиться, что операция

B* pB = dynamic_cast<B*> (pA)

не получится (потому что pA указывает на объект типа С, что и проверил dynamic_cast и вынес свой вердикт).

Ссылок никаких не привожу, источник — личный опыт.

Всем спасибо!
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 16

    +2
    Хорошо бы добавить результат работы каждого из приведенных блоков, чтобы не отвлекаться от чтения.
    Еще можно поиграться, с виртуальным наследование. Посмотреть, что и как располагается в памяти.
      0
      Думаю, не стоит без крайней необходимости и абсолютной уверенности использовать приведение от предка к наследнику. Ни static, ни dynamic.
      Да, dynamic_cast в большинстве случаев отработает более корректно, но, в любом случае, это будет ошибка проектирования, которая переносится из compile-time в runtime.
        +1
        dynamic_cast в большинстве случаев отработает более корректно

        Можно пример случая когда отработает некорректно?

          0

          С -no-rtti? Но и так он будет корректно работать для типов для которых это можно доказать в compile time

            0

            Ну -no-rtti — это всё-таки весьма специфический случай (да ещё и нестандартный).

            0

            Говорят в темные времена были проблемы с версиями компиляторов и библиотек, но я их не застал. Думаю костыли типа qobject_cast из тех же времен.

              +1
              Он не требует RTTI, но работает только с наследниками QObject и в класе должен был использован макрос Q_OBJECT. Подозреваю, что они его сделали для своих каких-то целей. Qt, на мой взгляд, не просто фреймворк/библиотека, а почти отдельный язык базирующийся на C++.
                0

                Насколько я знаю, qobject_cast скорее для скорости.

                  0

                  Такое же советуют и доке Qt. Но вроде бы исходно он возник из за кривости очень старых плюсов. Как и всякая хрень типа qMax

            +11
            struct A{
                A():a(0), b(0){}
              int a;
              int b;
            };
            struct B : A{

            ну вы чё, а?
            здорово, что вы, как автор, в контексте, и понимаете что a и b лежат в A, а B лежит g…
            но читать же нереально — это противоречит логике.
            пусть в A лежат a1 и a2, в B — b или b1. Сразу понятно кто к кому относится.

            offtopic
            я сюда деградировать захожу, а не транслировать переменные в голове

              0
              Вы описали мою первыю мысль, 4 часа назад прочитал статью про именование переменных и осмысленные названия всего, а тут на тебе мы вернулись в 8й класс и обзываем все однобуквенно и пусть лежат в А a,b; Но блин как понять читая код что В зависит от А и остальное наследование, это очень важно, отсюда и такие ошибки, для этого не обязательно использовать dynamic_cast для этого можно использовать меньше указателей и адекватные названия.
                0
                Дельное замечание, почему то не подумал заранее. Спасибо
                +1

                Вообще если понадобился dynamic_cast — время взять листок бумаги и заняться проектированием, что-то пошло не так. Обычно решается добавлением по иерархии ещё одной виртуальной функции, но где-то удобен visitor. А может — надо просто почистить и упростить код.

                  0

                  Хм… А у меня у одного последний пример не компилится?
                  error C2683: 'dynamic_cast': 'A' is not a polymorphic type

                    0

                    Читайте внимательнее.
                    "Для того, чтобы такая проверка была возможна, следует добавить к классам виртуальность (dynamic_cast использует таблицы виртуальных функций, чтобы делать проверку)."

                    0
                    Я бы еще отметил, что во-первых dynamic_cast работает в run-time, что влечет за собой накладные расходы и может сказаться на производительности (в Сети есть тесты, если интересно), а во-вторых, требует поддержки RTTI (в некоторых проектах ее осознанно отключают).

                    Only users with full accounts can post comments. Log in, please.