我正在将一个简单的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()进行虚拟调用。
这些类不是standard-layout,但不使用多重或虚拟继承。这保证有效吗?
请注意,如果重要的话,我可以控制所有代码(C ++和C包装器)。
答案 0 :(得分:4)
你当然可以为某些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函数传播!
答案 1 :(得分:3)
// 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*>
)答案 2 :(得分:1)
这是我过去使用的方法(也许是Aaron评论所暗示的)。请注意,相同的类型名称在C和C ++中都使用。转换都是用C ++完成的;无论合法性问题如何,这自然代表了良好的封装。 [显然你也需要delete
个方法。
请注意,要使用someOperation()
来呼叫Derived*
,需要明确向Base*
转播。 如果 Derived
没有提供任何新方法,例如someOtherOperation
,那么您不需要向客户端公开Derived*
,并避免客户端强制转换。< / p>
标题文件:“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();
}
答案 3 :(得分:0)
我认为这两行是问题的核心:
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代码。但我会担心编译器会允许各种疯狂的强制转换,例如{{1}或者甚至强制转换为不同类层次结构中的类型。我的目标是在C代码中强制执行适当的面向对象的 is-a 关系。)
然后,C句柄类可能类似于
(Derived1*) derived2_ptr
这样可以很容易地以简洁安全的方式使用类似强制转换的内容:请注意我们如何在此代码中传入struct base_t_ptr {
void * this_; // holds the Base pointer
};
typedef struct {
struct base_t_ptr get_base;
} derived_t_ptr;
:
object.get_base
其中base_some_operation的声明是
first_derived_t_ptr object = first_derived_create();
base_some_operation(object.get_base);
这将是非常类型安全的,因为如果不通过extern "C" base_some_operation(struct base_t_ptr);
数据成员,您将无法将derived1_t_ptr传递给此函数。它还可以帮助您的C代码了解类型和转换的有效性 - 您不希望意外地将Derived1转换为Derived2。
然后,当实现仅在派生类中定义的非虚拟方法时,您需要以下内容:
.get_base