Почему я должен использовать dynamic_cast здесь - PullRequest
5 голосов
/ 07 февраля 2020

Я заметил, что если я использую приведение в стиле C (или reinterpret_cast) в приведенном ниже коде, я получаю исключение ошибки сегментации, но если я использую dynamic_cast, это нормально. Почему это? Так как я уверен, что указатель a относится к типу B, потому что метод Add уже гарантирует, что ввод имеет тип B.

Должен ли я использовать dynamic_cast здесь, хотя я уже гарантирую, что в моей реализации указатель a относится к типу B?

Редактировать:

Я действительно понимаю, что в общем случае плохой практикой является использование стиля C (или reinterpret_cast). Но для ЭТОГО особого случая, почему они не работают.

Это имеет практическое применение, потому что если класс B является интерфейсом, а класс D вынужден хранить указатель типа A по какой-то причине. Здесь используется Dynami c cast, когда реализация уже гарантирует безопасность типов для типа интерфейса.

#include <iostream>

using namespace std;

class A
{
    public:
    virtual ~A() = default;
};

class B
{
    public:
    virtual string F() = 0;
};

class C : public A, public B
{
    public:
    virtual ~C() = default;
    virtual string F() { return "C";}
};

class D
{
    public:

    D() : a(nullptr) {}

    void Add(B* b)
    {
        A* obj = dynamic_cast<A*>(b);
        if(obj != nullptr)
            a = obj;
    }

    B* Get()
    {
        return (B*)(a); // IF I USE DYNAMIC CAST HERE, IT'D BE OK
    }

    private:
    A* a;
};

int main()
{
    D d;
    d.Add(new C());

    B* b = d.Get();
    if(b != nullptr)
        cout << b->F();
}

Ответы [ 4 ]

4 голосов
/ 07 февраля 2020

tl; dr : c Приведения в стиле являются хитрыми и могут легко вносить ошибки.

Так что же происходит в этом выражении?

class A
{
    public:
    virtual ~A() = default;
};

class B
{
    public:
    virtual string F() = 0;
};

B* Get()
{
    return (B*)(a);
}

Замечание что A и B не связаны между собой.

Что, если вместо этого использовать правильный static_cast?

B* Get()
{
    return static_cast<B*>(a);
}

После этого вы увидите правильную диагностику c:

error: invalid 'static_cast' from type 'A*' to type 'B*'
            return static_cast<B*>(a);
                   ^~~~~~~~~~~~~~~~~~

О, нет .

Действительно, c стиль отбрасывает откат с reinterpret_cast, когда состояние c можно не будет сделано Таким образом, ваш код эквивалентен:

B* Get()
{
    return reinterpret_cast<B*>(a);
}

Это не то, что вы хотите. Это не тот актер, который вы ищете.

Субобъект A имеет адрес, отличный от субобъекта B, в основном для размещения vtable.

Что именно reinterpret_cast здесь делает?

Не много, правда. Он просто указывает компилятору интерпретировать адрес памяти, отправленный ему, как другой тип. Это работает только в том случае, если у типа, который вы запрашиваете, есть время жизни по этому адресу. В вашем случае это не так, в этом месте есть объект A, часть вашего объекта B находится где-то в памяти.

Приведение типа stati c отрегулирует указатель, чтобы убедиться, что он указывает на правое смещение в памяти для этого типа и не может скомпилироваться, если не может вычислить смещение.

C* c = new C();
cout << c;
cout << "\n";

A* a = dynamic_cast<A*>(c);
cout << a;
cout << "\n";

B* b = dynamic_cast<B*>(c);
cout << b;
cout << "\n";

Даст вам нечто подобное:

0xbe3c20
0xbe3c20
0xbe3c28

Что можете ли вы сделать это тогда?

Если вы хотите использовать stati c cast, вам нужно будет от go до C, так как это единственное место, где компилятор может видеть отношения между A и B:

B* Get()
{
    return static_cast<B*>(static_cast<C*>(a));
}

Или, если вы не знаете, является ли C типом времени выполнения объекта, на который указывает a, вы должны использовать dynamic_cast.

1 голос
/ 08 февраля 2020
Приведение в стиле

C очень опасно, поэтому в c ++ у нас есть static_cast и reinterpret_cast (а также динамический c -cast, который предназначен только для c ++)

reinterpret_cast также Опасный, как c стиль приведения, и просто возьмет адрес для вашего B * и даст вам тот же адрес, что и A *, НЕ тот, который вы хотите.

static_cast требует, чтобы типы источника и назначения были связанные с. Вы не можете просто динамически передать B * на A *, потому что они не связаны. Однако он не выполняет никакой другой проверки, он просто применяет простое фиксированное математическое правило к адресу.

Вы можете использовать static_cast для C*, а затем для A *, и это будет допустимо и безопасно до тех пор, пока поскольку вы уверены, что ваш объект является C, в противном случае он будет go ужасно неправильным, даже если этот другой объект имеет элементы A и B, если он также имеет другие элементы, эти два объекта могут иметь разные смещения , а исправленная математика даст неправильный ответ.

dynamic_cast фактически просит сам объект помочь. Он размещен в реализации C*, которая знает оба типа: A и B. Если бы это был другой объект реализации, этот объект разрешил бы соответствующий ответ.

1 голос
/ 08 февраля 2020

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

A* a;
return (B*)(a);

Почему стиль C сбой приведения?

При приведении указателей (к объектам) приведение в стиле C имеет ту же функциональность, что и reinterpret_cast, плюс возможность отбрасывать const и volatile. См. Ниже, почему reinterpret_cast терпит неудачу.

Почему reinterpret_cast терпит неудачу?

A reinterpret_cast говорит компилятору обрабатывать выражение как если бы он имел новый тип . Используется один и тот же битовый шаблон, просто интерпретируемый по-разному. Это проблема при работе с составным объектом.

Данный объект имеет тип C, который является производным от A и B. Ни A, ни B не имеют объектов нулевого размера, что является ключевым фактором. (Классы могут выглядеть пустыми, но, поскольку они имеют виртуальные функции, каждый объект этих классов содержит указатель на таблицу виртуальных функций.) Вот одна из возможных компоновок, в которой мы предполагаем, что размер указателя равен 8:

----------------------------------
| C : | A : pointer to A's table |  <-- Offset 0
|     | B : pointer to B's table |  <-- Offset 8
----------------------------------

Ваш код начинается с указателя на C, который в итоге сохраняется как указатель на A. На изображении выше эти адреса численно совпадают. Все идет нормально. Затем вы берете этот адрес и говорите компилятору, что он является указателем на B, хотя подобъект B смещен на 8 байтов. Поэтому, когда вы go вызываете b->F(), программа ищет адрес F в таблице виртуальных функций A! Даже если это приведет к действительному указателю на функцию, вы увидите ошибку сегментации, если сигнатура этой функции не совпадает с сигнатурой B::F. (Другими словами, ожидайте cra sh.)

На ноте более pedanti c, поскольку A и B являются несвязанными типами с использованием указателя, созданного вашим актером. приводит к неопределенному поведению. Вышеприведенное просто объясняет, что обычно происходит в этом случае, но технически стандарт позволил бы получить результат «мой компьютер взорвался».

Почему работает dynamic_cast?

Короче говоря, dynamic_cast добавит 8 к указателю в момент нажатия клавиши. То, что вы пытаетесь сделать, известно как «sidecast», и это одна из вещей, для которых dynamic_cast предназначен. (Это 5b в объяснении cppreference о dynamic_cast.) dynamic_cast распознает, что то, на что указывает a, действительно имеет тип C (наиболее производный тип) и что C имеет однозначную базу типа B. Таким образом, приведение вычисляет разницу между смещениями A и B в объектах C и корректирует указатель. Смещение B равно 8, а смещение A равно 0, поэтому указатель корректируется на 8-0, в результате чего действительный указатель равен B.

. указатель на B фактически указывает на объект типа B, вызов виртуальной функции B работает.

Использование static_cast до go от C* до B* работает аналогично, но если, конечно, у вас нет C* для работы в этом случае.

0 голосов
/ 08 февраля 2020

Я преобразовал ваш код в более минималистичный пример c, удалив class D. Это облегчает эксперименты с вариациями, которые помогут уточнить, что работает, а что нет.

С точки зрения изложения, я просто скажу, что reinterpret_cast - это низкий уровень работа с очень специфической c и ограниченной утилитой. Он существует для случаев, когда вы хотите сообщить компилятору, что вы знаете что-то, чего он не знает - что побитовое значение может быть значимо переосмыслено как другой тип. Этого не может быть в случае вашего бокового приведения class C, поскольку множественные наследуемые базовые типы не могут оба существовать по одному и тому же адресу памяти.

#include <iostream>

using namespace std;

class A
{
    public:
    virtual ~A() {}
};

class B
{
    public:
    virtual string F() = 0;
};

class C : public A, public B
{
    public:
    virtual string F() { return "C";}
};

int main()
{

    C c;
    auto display = [&c](B* b) -> string
       {
       return ( dynamic_cast<C*>(b) == &c ) ? b->F() : "BROKEN";
       };

    B* b1 = static_cast<B*>(&c); // simple up conversion (cast optional)
    cout << "b1: " << display(b1) <<"\n";

    A* a2 = static_cast<A*>(&c); // simple up conversion (cast optional)
    B* b2 = dynamic_cast<B*>(a2); // dynamic sideways conversion
    cout << "b2: " << display(b2) <<"\n";

    A* a3 = reinterpret_cast<A*>(&c); // low-level conversion (happens to work)
    B* b3 = dynamic_cast<B*>(a3); // dynamic sideways conversion
    cout << "b3: " << display(b3) <<"\n";

    B* b4 = reinterpret_cast<B*>(&c); // low-level conversion (doesn't work)
    cout << "b4: " << display(b4) <<"\n";

    A* a5 = static_cast<A*>(&c);
    B* b5 = reinterpret_cast<B*>(&a5); // low-level conversion (doesn't work)
    cout << "b5: " << display(b5) << "\n";

}

Вывод

$ ./why-do-i-have-to-use-a-dynamic-cast-here.cpp 
b1: C
b2: C
b3: C
b4: BROKEN
Segmentation fault (core dumped)

Наиболее распространенный вариант использования для reinterpret_cast - это сценарий, в котором вы хотите манипулировать или сохранять значения в терминах их внутреннего представления - даже если это может выходить за рамки формальной спецификации C ++ (и следовательно, не portable ).

Знакомый пример - чтение значений данных из двоичного файла . std::istream::read требуется буфер типа char*, поэтому reinterpret_cast требуется для чтения в любой другой тип данных (как показано в документированном примере).


Похожие: Не удается выполнить динамический трансляцию в боковом направлении

...