I am wrapping a simple C++ inheritance hierarchy into "object-oriented" C. I'm trying to figure out if there any gotchas in treating the pointers to C++ objects as pointers to opaque C structs. In particular, under what circumstances would the derived-to-base conversion cause problems?
The classes themselves are relatively complex, but the hierarchy is shallow and uses single-inheritance only:
// 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..
I am planning on exposing this to C clients via the following, fairly standard object-oriented 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;
}
The C clients only pass around pointers to the underlying C++ objects and can only manipulate them via function calls. So the client can finally do something like:
first_derived_t* object = first_derived_create();
base_some_operation((base_t*) object); // Note the derived-to-base cast here
...
and have the virtual call to FirstDerived::someOperation() succeed as expected.
These classes are not standard-layout but do not use multiple or virtual inheritance. Is this guaranteed to work?
Note that I have control over all the code (C++ and the C wrapper), if that matters.
// 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;
}
This means that you can always perform a cast such as the following.
first_derived_t* object = first_derived_create();
base_some_operation((base_t*) object);
This is safe because the base_t*
pointer will be cast to void*
, then to Base*
. This is one step less than what happened before. Notice the order:
FirstDerived*
Base*
(via implicit static_cast<Base*>
)void*
(via static_cast<void*>
)first_derived_t*
(via reinterpret_cast<first_derived_t*>
)base_t*
(via (base_t*)
, which is a C++-style reinterpret_cast<base_t*>
)void*
(via implicit static_cast<void*>
)Base*
(via reinterpret_cast<Base*>
)For calls that wrap a FirstDerived
method, you get an extra cast:
FirstDerived*
(via dynamic_cast<FirstDerived*>
)