У меня есть структура 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);
}
Есть ли еще проблемы с этим или я должен сделать это совсем по-другому?
Требования выравнивания не должны вызывать проблем, и это действительно стандартный способ фальсифицировать наследование в 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));
}
Пока вы не представите виртуальную таблицу (добавив виртуальный метод или деструктор) в struct B
а также parent
член этой структуры является первым элементом, то это безопасно на всякий случай B
в A
,
Кастинг A
в B
тоже безопасно, но только если исходный указатель действительно указывает на B
а не к чему-то другому, как только A
Сама структура, очевидно.
В случае когда parent
не первый член структуры B
, вам придется использовать container_of
макрос, как видно в ядре Linux, например. Он работает от смещений указателей членов (то есть можно найти хорошее объяснение Вот).
Кроме того, по умолчанию обе структуры будут иметь одинаковое выравнивание. Я не уверен, как компилятор будет размещать A
в B
если вы настроите одну из структур выравнивания. Я предполагаю, что это зависит от компилятора, но должно быть легко выяснить.
Если B
имеет виртуальную таблицу, вы должны знать внутренние компоненты компилятора, чтобы правильно конвертировать указатели. Например, GCC
на x86_64 добавляет 8-байтовое смещение, поэтому вы должны сместить его вручную при конвертации. Но это не сработает в случае виртуального наследования (виртуальные базовые классы), чтобы решить, что вы должны прибегнуть к использованию RTTI и т. Д. Но это выходит за рамки вашего вопроса, и я бы порекомендовал вам не идти по этому пути.
Надеюсь, поможет.