dynamic_cast обратно к тому же типу объекта завершается ошибкой с множественным наследованием и промежуточной переменной

Предположим, есть иерархия с двумя несвязанными полиморфными классами PCH и GME, подклассом PCH_GME : public GME, public PCH и объектом gme_pch типа PCH_GME*.

Почему следующая последовательность приведения gme_pch "ломает" приведение к исходному типу объекта GME_PCH*:

GME_PCH *gme_pch = new GME_PCH();    
GME *gme = gme_pch;
PCH *pch = (PCH*)gme;
GME_PCH *same_as_gme_pch = dynamic_cast<GME_PCH*>(pch);
// same_as_gme_pch is NULL

тогда как следующий код не нарушает приведения:

GME_PCH *gme_pch = new GME_PCH();    
PCH *pch = gme_pch;    
GME_PCH *same_as_gme_pch = dynamic_cast<GME_PCH*>(pch);
// address of same_as_gme_pch == gme_pch

Вопрос: Разве каждый указатель не всегда указывает на один и тот же объект, и не должен ли результат окончательного приведения обратно к исходному типу всегда быть одним и тем же?

РЕДАКТИРОВАТЬ: согласно ответам я добавил вывод адресов gme_pch и pch. И показывает, что отношение этих двух указателей в работающих вариантах отличается от тех, где оно не работает (т.е. в зависимости от того, пишется ли GME_PCE : public GME, public PCH или GME_PCE : public PCH, public GME, gme_pch равно pch в рабочем варианте и gme_pch не равно в нерабочем вариантов и наоборот).


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

class PCH {  // PrecachingHint
public:
    virtual std::string getHint() const = 0;
};

class GME {  // GenericModelElement
public:
    virtual std::string getKey() const = 0;
};

class GME_PCH : public GME, public PCH {
public:
    virtual std::string getHint() const { return "some hint"; }
    virtual std::string getKey() const { return "some key"; }
};

void castThatWorks() {

    GME_PCH *gme_pch = new GME_PCH();

    PCH *pch = gme_pch;

    GME_PCH *same_as_gme_pch = dynamic_cast<GME_PCH*>(pch);

    std::cout << ((same_as_gme_pch == nullptr) ? "cast did not work." : "cast worked.")<< "gmepch:" << gme_pch << "; pch:" << pch << std::endl;
}

void castThatWorks2() {

    GME_PCH *gme_pch = new GME_PCH();

    GME *gme = gme_pch;
    PCH *pch = dynamic_cast<PCH*>(gme);

    GME_PCH *same_as_gme_pch = dynamic_cast<GME_PCH*>(pch);

    std::cout << ((same_as_gme_pch == nullptr) ? "cast did not work." : "cast worked.")<< "gmepch:" << gme_pch << "; pch:" << pch << std::endl;
}

void castThatDoesntWork() {

    GME_PCH *gme_pch = new GME_PCH();

    GME *gme = gme_pch;  // note: void* gme = gme_pch breaks the subsequent dynamic cast, too.
    PCH *pch = (PCH*)gme;

    GME_PCH *same_as_gme_pch = dynamic_cast<GME_PCH*>(pch);

    std::cout << ((same_as_gme_pch == nullptr) ? "cast did not work." : "cast worked.")<< "gmepch:" << gme_pch << "; pch:" << pch << std::endl;
}

void castThatDoesntWork2() {

    GME_PCH *gme_pch = new GME_PCH();

    GME *gme = gme_pch;
    PCH *pch =  reinterpret_cast<PCH*>(gme);

    GME_PCH *same_as_gme_pch = dynamic_cast<GME_PCH*>(pch);

    std::cout << ((same_as_gme_pch == nullptr) ? "cast did not work." : "cast worked.")<< "gmepch:" << gme_pch << "; pch:" << pch << std::endl;
}

void castThatDoesntWork3() {

    GME_PCH *gme_pch = new GME_PCH();

    GME *gme = gme_pch;
    PCH *pch = static_cast<PCH*>(static_cast<void*>(gme));

    GME_PCH *same_as_gme_pch = dynamic_cast<GME_PCH*>(pch);

    std::cout << ((same_as_gme_pch == nullptr) ? "cast did not work." : "cast worked.")<< "gmepch:" << gme_pch << "; pch:" << pch << std::endl;
}

int main() {
    castThatWorks();
    castThatWorks2();
    castThatDoesntWork();
    castThatDoesntWork2();
    castThatDoesntWork3();   
}

Выход:

cast worked.gmepch:0x100600030; pch:0x100600038
cast worked.gmepch:0x100600040; pch:0x100600048
cast did not work.gmepch:0x100600260; pch:0x100600260
cast did not work.gmepch:0x100202c30; pch:0x100202c30
cast did not work.gmepch:0x100600270; pch:0x100600270

person Stephan Lechner    schedule 22.07.2018    source источник
comment
Обратите внимание, что в нерабочих версиях используется reinterpret_cast или эквивалент, тогда как в рабочих версиях используется только неявное преобразование и dynamic_cast.   -  person M.M    schedule 23.07.2018


Ответы (4)


GME_PCH *gme_pch = new GME_PCH();

GME *gme = gme_pch;
PCH *pch = static_cast<PCH*>(static_cast<void*>(gme));

неверно, когда GME_PCH наследуется как от GME, так и от PCH. Не используйте его.

Вы заметите, почему это неправильно, попробовав следующее:

GME_PCH *gme_pch = new GME_PCH();

GME *gme = gme_pch;
PCH *pch1 = gme_pch;  // Implicit conversion. Does the right offsetting of pointer
PCH *pch2 = static_cast<PCH*>(static_cast<void*>(gme)); // Wrong.

std::cout << "pch1: " << pch1 << ", pch2: " << pch2 << std::endl;

Вы заметите, что pch1 и pch2 разные. pch1 является допустимым значением, а pch2 — нет.

person R Sahu    schedule 22.07.2018
comment
Твое право. Я добавил вывод, который показывает, что значения указателя в рабочем варианте ведут себя иначе, чем в нерабочем. Спасибо. - person Stephan Lechner; 24.07.2018

Когда вы конвертируете GME_PCH * в PCH * с помощью неявного преобразования, static_cast или dynamic_cast, результат указывает на подобъект PCH объекта GME_PCH.

Однако, когда вы конвертируете GME_PCH * в PCH * с помощью reinterpret_cast, результат оставляет адрес неизменным: он по-прежнему указывает на место в памяти объекта GME_PCH, где обычно находится подобъект GME (компиляторы обычно размещают полиморфные объекты с первым базовый класс первым в памяти).

введите здесь описание изображения

Все ваши нерабочие попытки эквивалентны reinterpret_cast<PCH *>(gme_pch). Они терпят неудачу, потому что вы получаете указатель типа PCH *, который не указывает на объект PCH.


Приведение в стиле C ведет себя как static_cast, если это допустимо, в противном случае оно ведет себя как reinterpret_cast.

Код (PCH *)gme_pch — это static_cast<PCH *>(gme_pch), а код (PCH *)gme — это reinterpret_cast<PCH *>(gme).

Чтобы добраться до PCH из GME, вам нужно использовать dynamic_cast, который способен проверить, действительно ли GME является частью GME_PCH или нет. Если это не так, приведение даст нулевой указатель.

person M.M    schedule 23.07.2018

PCH *pch = (PCH*)gme;

Прекратите использовать приведения в стиле C. Эта строка кода не делает ничего разумного; он переинтерпретирует биты gme как указатель на одну вещь и говорит: «Что, если бы эти биты относились к другому типу».

Но адреса подобъектов GME и PCH различны, поэтому полученный указатель — мусор. Тогда все остальное терпит неудачу.

Строка также может быть записана как PCH *pch = reinterpret_cast<PCH*>(gme); Приведения в стиле C могут быть разумными или опасными.

Это PCH *pch = static_cast<PCH*>(static_cast<void*>(gme)); нарушает другое правило; при приведении к void* вы всегда должны возвращать к тому же типу, из которого вы выполняли приведение.

Есть случаи, когда переинтерпретация приведения (или неправильное срабатывание через пустоту) работает; но они хрупкие и включают относительно эзотерический текст в стандарте.

Просто всегда возвращайте void ptr к его точному исходному типу и никогда не переинтерпретируйте указатели приведения или приведения в стиле C к другим типам.

person Yakk - Adam Nevraumont    schedule 23.07.2018

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

struct foo
{
    int whatever;
};

struct bar: public foo
{
    virtual void what();
};

расположение бара:

pvtable ← bar * укажет здесь

int ← foo * укажет здесь

Более того, в случае виртуального наследования будет невозможно определить правильную настройку указателя, поэтому фактическое значение указателя базового класса явно сохраняется в каждом экземпляре производного класса.

person user7860670    schedule 22.07.2018
comment
поэтому фактическое значение указателя базового класса явно сохраняется в каждом экземпляре производного класса. Не всегда. Его можно найти через vptr - person curiousguy; 23.07.2018