Поддельное наследство в C: сломает ли выравнивание мою шею?

У меня есть структура C, которая используется в различных C и C ++ код (через extern "C").

#ifdef __cplusplus
extern "C" {
#endif

typedef struct A A;
struct A {
/*some members*/
};

#ifdef __cplusplus
}
#endif

Распределение, инициализация и освобождение выполняются отдельными функциями-членами, находящимися под моим контролем, но я не контролирую доступ к элементам, так как они доступны повсеместно.

Проблема в том, что я не могу изменить structопределение в заголовке, которое интенсивно используется во всей системе, но я все еще хочу расширить тип и добавить некоторые члены. Поскольку это должно компилироваться как в C ++, так и в C, я не могу просто создать производный тип struct B : public A, Поэтому я хотел добавить этот тип в файл cpp:

#ifdef __cplusplus
extern "C" {
#endif

typedef struct B B;
struct B {
A parent; // <---- public inheritance
/*some members*/
};

#ifdef __cplusplus
}
#endif

Теперь я могу изменить все функции в файле cpp, но мне все еще нужно раздать и принять A* за пределами модуля компиляции, так как никто не знает, что B является.

Итак, мне интересно, есть ли здравый и четко определенный способ сделать это. Могу ли я просто бросить свой B*с A*s и обратно, или я должен их явно конвертировать:

A* static_cast_A(B* b) {
return &(b->parent);
}
B* static_cast_B(A* a) {
B* const b = 0;
unsigned const ptrdiff = (unsigned)((void*)(&(b->parent)));
return (B*)(((void*)a)-ptrdiff);
}

Есть ли еще проблемы с этим или я должен сделать это совсем по-другому?

4

Решение

Требования выравнивания не должны вызывать проблем, и это действительно стандартный способ фальсифицировать наследование в C.

Тем не менее, ваш static_cast_B() не является портативным (в частности, void * арифметика и преобразование в unsigned в случае sizeof (void *) > sizeof (unsigned) — последний должен все еще работать, но это что-то вроде запаха кода).

Я предлагаю использовать следующий код:

#include <stddef.h>

B* static_cast_B(A* a) {
return (B*)((char*)a - offsetof(B, parent));
}
2

Другие решения

Пока вы не представите виртуальную таблицу (добавив виртуальный метод или деструктор) в struct B а также parent член этой структуры является первым элементом, то это безопасно на всякий случай B в A,

Кастинг A в B тоже безопасно, но только если исходный указатель действительно указывает на B а не к чему-то другому, как только A Сама структура, очевидно.

В случае когда parent не первый член структуры B, вам придется использовать container_of макрос, как видно в ядре Linux, например. Он работает от смещений указателей членов (то есть можно найти хорошее объяснение Вот).

Кроме того, по умолчанию обе структуры будут иметь одинаковое выравнивание. Я не уверен, как компилятор будет размещать A в B если вы настроите одну из структур выравнивания. Я предполагаю, что это зависит от компилятора, но должно быть легко выяснить.

Если B имеет виртуальную таблицу, вы должны знать внутренние компоненты компилятора, чтобы правильно конвертировать указатели. Например, GCC на x86_64 добавляет 8-байтовое смещение, поэтому вы должны сместить его вручную при конвертации. Но это не сработает в случае виртуального наследования (виртуальные базовые классы), чтобы решить, что вы должны прибегнуть к использованию RTTI и т. Д. Но это выходит за рамки вашего вопроса, и я бы порекомендовал вам не идти по этому пути.

Надеюсь, поможет.

4