#c #pointers #multiple-inheritance #vtable #memory-alignment
#c #указатели #множественное наследование #vtable #выравнивание по памяти
Вопрос:
Допустим, у нас есть конкретный класс A и абстрактный класс B.
Рассмотрим конкретный C, который наследует как от A, так и от B и реализует B:
class C : public A, public B
{
/* implementation of B and specific stuff that belongs to C */
};
Теперь я определяю функцию, сигнатура которой void foo(B* b);
Это мой код, я могу предположить, что все указатели на B являются как A, так и B. В определении foo, как получить указатель на A? Неприятный, но работающий трюк заключается в выравнивании обратных указателей следующим образом:
void foo(B* b)
{
A* a = reinterpret_cast<A*>(reinterpret_cast<char*>(b) - sizeof(A));
// now I can use all the stuff from A
}
Имейте в виду, что C не имеет супертипа и на самом деле, существует множество классов, похожих на C, которые являются только A и B. Не стесняйтесь подвергать сомнению как мою логику, так и этот образец дизайна, но вопрос касается только выравнивания указателей.
Комментарии:
1. Результат любой попытки получить
A
изB
a почти наверняка неопределен . Почему вы хотите это сделать?2. В данном случае это идеально определено, поскольку я уверен, что получу указатель на B из экземпляра C.
Ответ №1:
void foo(B* b)
{
//A* a = reinterpret_cast<A*>(reinterpret_cast<char*>(b) - sizeof(A)); // undefined behaviour!!!!
A* a = dynamic_cast<A*>(b);
if (a)
{
// now I can use all the stuff from A
}
else
{
// that was something else, not descended from A
}
}
Забыл сказать: для выполнения динамического приведения оба A и B должны иметь виртуальные функции или, по крайней мере, виртуальные деструкторы. В противном случае нет законного способа выполнить это преобразование типов.
Комментарии:
1. @Oli Charlesworth: Я не думаю, что вы знакомы с C достаточно, чтобы делать такие выводы.
2. @Jurlie: Тогда попробуйте скомпилировать это.
3. @Oli Charlesworth : Это компилируется и работает нормально, пока оба
A
иB
являются полиморфными (т. Е. имеют виртуальную таблицу). Я не уверен, в чем заключается ваше утверждение…4. @mister почему: Имейте в виду, что, хотя этот подход может работать, это совершенно глупо. Я бы настоятельно рассмотрел подход, предложенный sharptooth.
5. @Jurlie: Здесь этого не будет, потому что тип (
A*
илиauto
в наши дни) перед именем переменной делает его однозначным. Если переменная была объявлена ранее, а вы только что написалиif (a = ...)
, то компилятор выдал бы предупреждение.
Ответ №2:
Наличие огромного набора несвязанных классов, которые являются производными от A
и B
, является очень странным дизайном. Если есть что-то, что делает A
и B
всегда будет «использоваться вместе», вы могли бы либо объединить их, либо ввести класс shim, который является производным только от них, а затем только от этого класса:
class Shim : A, B {};
class DerivedX : Shim {};
и в последнем случае вы просто используете static_cast
для первого понижения от A
или B
до Shim*
, а затем C неявно преобразует Shim*
указатель в другой класс.
Комментарии:
1. Я понял вас, но я не предоставил весь дизайн. На самом деле B не всегда является одним и тем же абстрактным классом. Это может быть либо абстрактный B1, либо абстрактный B2, которые оба наследуются от B. Когда я нахожусь в foo, я не знаю, какой C у меня есть. Хороший дизайн, основанный на шаблонах, заставил бы меня использовать идиому CRTP , которая определенно неудобна по особым причинам.
Ответ №3:
Если вы хотите использовать функциональность как класса A, так и класса B в своей функции, вам следует модифицировать функцию для получения указателей C:
void foo(C* c);
И в целом вы ошибаетесь в своем предположении, что «каждый B также является A». Вы могли бы создавать классы, производные от вашего интерфейса B, а не производные от класса A, вот почему компилятор не будет знать, что в вашем конкретном случае «каждый B является A».
Комментарии:
1. Что ж, основываясь на вашем комментарии к предыдущему сообщению, я думаю, что следует изучить вашу функцию foo, потому что это могут быть две разные функции (одна работает с потомками B, а другая — с классом A), и они должны вызываться одна за другой.
Ответ №4:
Расширяя ответ sharptooth (и вводя его как ответ, потому что я не могу вставить форматированный код в комментарий), вы все равно можете использовать прокладку:
class Shim : public virtual A, public virtual B {};
Затем:
class Derived1 : public Shim, public virtual A, public virtual B1
{
};
class Derived2 : public Shim, public virtual A, public virtual B2
{
};
B1
и B2
должны выводиться виртуально из B
.
Но я подозреваю, что если вам всегда нужно реализовывать оба A
и B
, вам следует создать единый интерфейс с обоими, либо путем наследования, либо объединив оба в один класс; ваш B1
and B2
унаследует от этого. (Решение с dynamic_cast
, конечно, предназначено для случая, когда производный класс B
может быть также производным от A
, а может и не быть.)