Как выровнять указатели при работе с множественным наследованием?

#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 , а может и не быть.)