Неправильно ли кастуется Дерив.Какая альтернатива? - PullRequest
0 голосов
/ 11 февраля 2019

Context

Моя цель - создать класс базового контейнера, который содержит и управляет несколькими объектами базового класса, а затем класс производного контейнера, который содержит и управляет несколькими объектами производного класса.По совету этого ответа я попытался сделать это, каждый из которых содержит массив указателей (Base** и Derived**) и приведен от Derived** к Base** при инициализациибазовый контейнерный класс.

Однако я столкнулся с проблемой - несмотря на то, что при компиляции все нормально, при манипулировании содержащимися объектами у меня возникли бы ошибки segfaults или были бы вызваны неправильные методы .


Проблема

Я свел проблему к следующему минимальному случаю:

#include <iostream>

class Base1 {
public:
    virtual void doThing1() {std::cout << "Called Base1::doThing1" << std::endl;}
};

class Base2 {
public:
    virtual void doThing2() {std::cout << "Called Base2::doThing2" << std::endl;}
};

// Whether this inherits "virtual public" or just "public" makes no difference.
class Derived : virtual public Base1, virtual public Base2 {};

int main() {
    Derived derived;
    Derived* derivedPtrs[] = {&derived};
    ((Base2**) derivedPtrs)[0]->doThing2();
}

Вы можете ожидать, что это выведет "Called Base2::doThing2", но…

$ g++ -Wall -Werror main.cpp -o test && ./test
Called Base1::doThing1

В самом деле - код вызывает Base2::doThing2, но Base1::doThing1 в итоге вызывает .У меня также был этот segfault с более сложными классами, так что я предполагаю, что он связан с адресами (возможно, с vtable - ошибка, похоже, не возникает без virtual методов).Вы можете запустить его здесь и увидеть сборку, к которой он компилируется, здесь .

Вы можете увидеть мою фактическую структуру здесь - она ​​более сложная, но он связывает это с контекстом и объясняет, почему мне нужно что-то вроде этого.

Почему приведение Derived**Base** не работает должным образом, когда Derived*Base* делает и, что более важно, как правильно обрабатывать массив производных объектов как массив базовых объектов (или, если это не так, еще один способ создать класс контейнера, который может содержатьнесколько производных объектов)?

Я не могу индексировать перед апкастингом (((Base2*) derivedPtrs[0])->doThing2()), я боюсь, поскольку в полном коде этот массив является членом класса - и я не уверен, что это хорошоИдея (или даже возможная) для приведения вручную в каждом месте содержащиеся объекты используются в контейнерах классов.Поправьте меня, если это способ справиться с этим, хотя.

(Я не думаю, что это имеет значение в этом случае, но я нахожусь в среде, где std::vectorнедоступен.)


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

Во многих ответах предполагается, что приведение каждого указателя по отдельности - единственный способ получить массив, который может содержать производныеобъекты - и это действительно так.Однако для моего конкретного случая использования мне удалось решить проблему с помощью templates !Предоставляя параметр типа для того, что должен содержать контейнерный класс, вместо того, чтобы содержать массив производных объектов, тип массива может быть установлен на производный тип во время компиляции (например, BaseContainer<Derived> container(length, arrayOfDerivedPtrs);).

Ниже приведена версия неработающего кода "фактической структуры", исправленного с помощью шаблонов.

Ответы [ 5 ]

0 голосов
/ 11 февраля 2019

Как и другие говорили, проблема в том, что вы не позволяете компилятору выполнять свою работу по корректировке индексов, предположим, что производное расположение в памяти является чем-то вроде (не гарантируется стандартом, только возможная реализация):

| vtable_Base1 | Base1 | vtable_Base2 | Base2 | vtable_Derived | Derived |

Затем &derived указывает на начало объекта, когда вы обычно делаете

Base2* base = static_cast<Derived*>(&derived)

, компилятор знает смещение, которое структура Base2 имеет внутри типа Derived, и корректирует адрес для указанияк началу.

Если вместо этого вы приведете массив указателей непосредственно, то компилятор применит тип, предполагая, что ваш массив уже хранит указатели на Base2, но без их корректировки.

Грязный хак, который может работать или не работать в вашей ситуации, имеет метод, который возвращает указатель на себя, например:

class Base2 {
public:
  Base2* base2() { return this; }
}

, чтобы вы могли сделать derivedPtrs[0]->base2()->doThing2().

0 голосов
/ 11 февраля 2019

Это не работает по той же причине, по которой это не работает:

struct Base {};

struct Derived : Base { int i; };

int main() {
    Derived d[6];
    Derived* d2 = d;
    Base** b = &d2; // ERROR!
}

Ваше приведение в стиле c - плохая практика, потому что оно не предупредило вас об ошибке.Только не делай этого со своим кодом.Ваше приведение в стиле c было на самом деле замаскированным reinterpret_cast, что в данном случае совершенно неверно.

Но почему вы не можете преобразовать массив в производное в массив в базу?Все просто: они имеют разную компоновку.

Видите ли, когда вы выполняете итерацию в массиве типа, каждый элемент в массиве непрерывен в памяти.Класс Derived может иметь размер, скажем, 24 байта, а Base размер 8:

Derived d[4];
------------------------------------------------------
|     D1     |     D2     |     D3     |      D4     |
------------------------------------------------------

Base b[4];
---------------------
| B1 | B2 | B3 | B4 |
---------------------

Как видите, Derived[4] и Base[4] - это разные типы сразличные макеты.


Тогда что вы можете сделать?

На самом деле существует множество решений.Проще всего было бы создать новый массив указателей на базу и привести каждый производный к базовым указателям.В любом случае вы должны отрегулировать указатель каждого объекта.

Это выглядело бы так:

std::vector<Base*> bases;
bases.reserve(std::size(derived_arr))

std::transform(
    std::begin(derived_arr), std::end(derived_arr),
    std::back_inserter(bases), 
    [](Dervied* d) {
        // You must use dynamic cast because the 
        // pointer offset in only known at runtime
        // when using virtual inheritance
        return dynamic_cast<Base*>(d); 
    }
);

Другое решение, которое существует в памяти, - создать собственный тип итератора.это будет делать бросок при вызове operator* и operator->.Это немного сложнее сделать, но можно сэкономить распределение, сделав итерацию немного медленнее.


Кроме того, это может быть не связано, но я бы посоветовал не использовать виртуальное наследование.Это не очень хорошая практика, и, по моему опыту, это привело к большей боли, чем к чему-либо еще.Я бы предложил использовать шаблон адаптера и вместо этого оборачивать неполиморфные типы.

0 голосов
/ 11 февраля 2019

Есть много вещей, которые делают этот код довольно ужасным и способствуют этой проблеме:

  1. Почему мы имеем дело с двухзвездными типами в первую очередь?Если std::vector не существует, почему бы не написать свой собственный?

  2. Не используйте приведение в стиле C.Вы можете приводить указатели на совершенно не связанные друг с другом типы, и компилятору не разрешается останавливать вас (по совпадению, это именно то, что здесь происходит).Вместо этого используйте static_cast / dynamic_cast.

  3. Предположим, у нас было std::vector, для простоты обозначения.Вы пытаетесь разыграть std::vector<Derived*> до std::vector<Base*>.Это не связанные типы (то же самое верно для Derived** и Base**), и приведение одного к другому не разрешено никоим образом.

  4. Приведение указателя от / к производномуне обязательно тривиальны.Если у вас есть struct X : A, B {}, то указатель на базу B будет отличаться от указателя на A базу (и с vtable в игре, возможно, также отличается от указателя наX).Они должны быть, потому что (под) объекты не могут находиться по одному адресу памяти.Компилятор будет корректировать значение указателя при его приведении.Это, конечно, не происходит / не может происходить для каждого отдельного указателя, если вы (пытаетесь) привести массив указателей.

Если у вас есть массив указателей на Derived и вы хотитеполучите массив этих указателей на Base, затем вам придется вручную привести каждый из них.Так как значения указателей, как правило, будут разными в обоих массивах, нет способа «повторно использовать» один и тот же массив.* выполнены, что не так для вас).

0 голосов
/ 11 февраля 2019

(Base2 **) на самом деле reinterpret_cast (Вы можете подтвердить это, попробовав все четыре приведения), и это выражение вызывает UB.Тот факт, что вы можете неявно приводить указатель к производному указателю на базу, не означает, что они одинаковы, например, int и float.И здесь вы ссылаетесь на объект по типу, который в данном случае не вызывает UB.

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

Предположим, что компилятор использует "vtable", находя vtable из Base2 (возможно, добавление смещения в память. assembly )из памяти адрес Derived не является тривиальным.

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

0 голосов
/ 11 февраля 2019

Когда вы приводите от Derived* до Base*, компилятор корректирует значение.Когда вы разыгрываете Derived** на Base**, вы побеждаете это.

Это хорошая причина всегда использовать static_cast.Пример ошибки с измененным кодом:

test.cpp:19:5: error: static_cast from 'Derived **' to 'Base2 **' is not
      allowed
    static_cast<Base2**>(derivedPtrs)[0]->doThing2();
...