Обтекание C++ в C: Производные к базовым преобразованиям
Я оборачиваю простую иерархию наследования C++ в "объектно-ориентированную" C. Я пытаюсь выяснить, есть ли какие-то хитрости при обработке указателей на объекты C++ как указателей на непрозрачные структуры C. В частности, при каких обстоятельствах преобразование из базы в базу данных может вызвать проблемы?
Сами классы относительно сложны, но иерархия невелика и использует только одиночное наследование:
// A base class with lots of important shared functionality
class Base {
public:
virtual void someOperation();
// More operations...
private:
// Data...
};
// One of several derived classes
class FirstDerived: public Base {
public:
virtual void someOperation();
// More operations...
private:
// More data...
};
// More derived classes of Base..
Я планирую раскрыть это клиентам C через следующую, довольно стандартную объектно-ориентированную C:
// An opaque pointers to the types
typedef struct base_t base_t;
typedef struct first_derived_t first_derived_t;
void base_some_operation(base_t* object) {
Base* base = (Base*) object;
base->someOperation();
}
first_derived_t* first_derived_create() {
return (first_derived_t*) new FirstDerived();
}
void first_derived_destroy(first_derived_t* object) {
FirstDerived* firstDerived = (FirstDerived*) object;
delete firstDerived;
}
Клиенты C передают только указатели на базовые объекты C++ и могут манипулировать ими только через вызовы функций. Таким образом, клиент может, наконец, сделать что-то вроде:
first_derived_t* object = first_derived_create();
base_some_operation((base_t*) object); // Note the derived-to-base cast here
...
и сделать виртуальный вызов FirstDerived::someOperation() успешным, как и ожидалось.
Эти классы не являются стандартным макетом, но не используют множественное или виртуальное наследование. Это гарантированно сработает?
Обратите внимание, что у меня есть контроль над всем кодом (C++ и оболочка C), если это имеет значение.
4 ответа
// An opaque pointers to the types
typedef struct base_t base_t;
typedef struct first_derived_t first_derived_t;
// **********************//
// inside C++ stub only. //
// **********************//
// Ensures you always cast to Base* first, then to void*,
// then to stub type pointer. This enforces that you'll
// get consistent a address in presence of inheritance.
template<typename T>
T * get_stub_pointer ( Base * object )
{
return reinterpret_cast<T*>(static_cast<void*>(object));
}
// Recover (intermediate) Base* pointer from stub type.
Base * get_base_pointer ( void * object )
{
return reinterpret_cast<Base*>(object);
}
// Get derived type pointer validating that it's actually
// the right type. Returs null pointer if the type is
// invalid. This ensures you can detect invalid use of
// the stub functions.
template<typename T>
T * get_derived_pointer ( void * object )
{
return dynamic_cast<T*>(get_base_pointer(object));
}
// ***********************************//
// public C exports (stub interface). //
// ***********************************//
void base_some_operation(base_t* object)
{
Base* base = get_base_pointer(object);
base->someOperation();
}
first_derived_t* first_derived_create()
{
return get_stub_pointer<first_derived_t>(new FirstDerived());
}
void first_derived_destroy(first_derived_t* object)
{
FirstDerived * derived = get_derived_pointer<FirstDerived>(object);
assert(derived != 0);
delete firstDerived;
}
Это означает, что вы всегда можете выполнить приведение типа следующего.
first_derived_t* object = first_derived_create();
base_some_operation((base_t*) object);
Это безопасно, потому что base_t*
указатель будет приведен к void*
затем Base*
, Это на один шаг меньше, чем было раньше. Обратите внимание на заказ:
FirstDerived*
Base*
(через неявныйstatic_cast<Base*>
)void*
(с помощьюstatic_cast<void*>
)first_derived_t*
(с помощьюreinterpret_cast<first_derived_t*>
)base_t*
(с помощью(base_t*)
в стиле C++reinterpret_cast<base_t*>
)void*
(через неявныйstatic_cast<void*>
)Base*
(с помощьюreinterpret_cast<Base*>
)
Для звонков, которые обернуть FirstDerived
Метод, вы получаете дополнительный бросок:
FirstDerived*
(с помощьюdynamic_cast<FirstDerived*>
)
Вы, конечно, можете создать интерфейс C для некоторого кода C++. Все, что тебе нужно extern "C"
и я рекомендую void *
как ваш непрозрачный тип данных:
// library.h, for C clients
typedef void * Handle;
extern "C" Handle create_foo();
extern "C" void destroy_foo(Handle);
extern "C" int magic_foo(Handle, char const *);
Затем реализуйте это в C++:
#include "library.h"
#include "foo.hpp"
Handle create_foo()
{
Foo * p = nullptr;
try { p = new Foo; }
catch (...) { p = nullptr; }
return p
}
void destroy_foo(Handle p)
{
delete static_cast<Foo*>(p);
}
int magic_foo(Handle p, char const * s)
{
Foo * const f = static_cast<Foo*>(p);
try
{
f->prepare();
return f->count_utf8_chars(s);
}
catch (...)
{
return -1;
errno = E_FOO_BAR;
}
}
Не забывайте никогда не разрешать никаким исключениям распространяться через вызывающую функцию C!
Это подход, который я использовал в прошлом (возможно, как подразумевается в комментарии Аарона). Обратите внимание, что одинаковые имена типов используются как в C, так и в C++. Все приведения сделаны в C++; это естественно представляет собой хорошую инкапсуляцию независимо от вопросов законности. [Очевидно, вам нужно delete
методы также.] Обратите внимание, что для вызова someOperation()
с Derived*
явный перевес в Base*
необходимо. Если Derived
не предоставляет никаких новых методов, таких как someOtherOperation
то вам не нужно выставлять Derived*
для клиентов, и избегать кастингов на стороне клиента.
Заголовочный файл:"BaseDerived.H"
#ifdef __cplusplus
extern "C"
{
#endif
typedef struct Base Base;
typedef struct Derived Derived;
Derived* createDerived();
Base* createBase();
Base* upcastToBase(Derived* derived);
Derived* tryDownCasttoDerived(Base* base);
void someOperation(Base* base);
void someOtherOperation(Derived* derived);
#ifdef __cplusplus
}
#endif
Реализация: "BaseDerived.CPP"
#include "BaseDerived.H"
struct Base
{
virtual void someOperation()
{
std::cout << "Base" << std::endl;
}
};
struct Derived : public Base
{
public:
virtual void someOperation()
{
std::cout << "Derived" << std::endl;
}
private:
};
Derived* createDerived()
{
return new Derived;
}
Base* createBase()
{
return new Base;
}
Base* upcastToBase(Derived* derived)
{
return derived;
}
Derived* tryDownCasttoDerived(Base* base)
{
return dynamic_cast<Derived*>(base);
}
void someOperation(Base* base)
{
base->someOperation();
}
void someOperation(Derived* derived)
{
derived->someOperation();
}
Я думаю, что эти две строки являются частью вопроса:
first_derived_t* object = first_derived_create();
base_some_operation((base_t*) object); // Note the derived-to-base cast here
...
Нет действительно безопасного способа разрешить это в C-коде. В C такое приведение никогда не изменяет необработанное целочисленное значение указателя, но иногда приведение C++ будет делать это, и поэтому вам нужен дизайн, который никогда не имеет приведений в C-коде.
Вот одно (слишком сложное?) Решение. Во-первых, определитесь с политикой, что код C всегда будет строго иметь дело со значением, которое фактически Base*
- это несколько произвольная политика для обеспечения последовательности. Это означает, что код C++ иногда должен быть в dynamic_cast, мы вернемся к этому позже.
(Вы можете заставить проект работать корректно с кодом C, просто используя приведения, как уже упоминалось другими. Но я бы беспокоился, что компилятор допускает всевозможные сумасшедшие приведения, такие как (Derived1*) derived2_ptr
или даже приводит к типам в другой иерархии классов. Моя цель здесь состоит в том, чтобы обеспечить надлежащие объектно-ориентированные отношения в коде C.)
Тогда классы дескриптора C могут быть чем-то вроде
struct base_t_ptr {
void * this_; // holds the Base pointer
};
typedef struct {
struct base_t_ptr get_base;
} derived_t_ptr;
Это должно облегчить использование чего-то вроде приведения кратким и безопасным способом: обратите внимание, как мы передаем object.get_base
в этом коде:
first_derived_t_ptr object = first_derived_create();
base_some_operation(object.get_base);
где объявление base_some_operation является
extern "C" base_some_operation(struct base_t_ptr);
Это будет достаточно безопасно для типов, так как вы не сможете передать производный_т_птр в эту функцию, не пройдя через .get_base
элемент данных. Это также поможет вашему коду C узнать немного о типах и о том, какие преобразования допустимы - вы не хотите случайно конвертировать Derived1 в Derived2.
Затем при реализации не виртуальных методов, определенных только в производном классе, вам понадобится что-то вроде:
extern "C" void derived1_nonvirtual_operation(struct derived1_t_ptr); // The C-style interface. Type safe.
void derived1_nonvirtual_operation(struct derived1_t_ptr d) {
// we *know* this refers to a Derived1 type, so we can trust these casts:
Base * bp = reinterpret_cast<Base*>(d.get_base.this_);
Derived1 *this_ = dynamic_cast<Derived1*>;
this_ -> some_operation();
}