5

我将一个简单的 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

4 回答 4

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;
}

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:

  1. FirstDerived*
  2. Base* (via implicit static_cast<Base*>)
  3. void* (via static_cast<void*>)
  4. first_derived_t* (via reinterpret_cast<first_derived_t*>)
  5. base_t* (via (base_t*), which is a C++-style reinterpret_cast<base_t*>)
  6. void* (via implicit static_cast<void*>)
  7. Base* (via reinterpret_cast<Base*>)

For calls that wrap a FirstDerived method, you get an extra cast:

  1. FirstDerived* (via dynamic_cast<FirstDerived*>)
于 2012-02-07T00:52:49.897 回答
4

您当然可以为某些 C++ 代码创建 C 接口。您只需要extern "C",我推荐 avoid *作为您的不透明数据类型:

// 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 函数传播!

于 2012-02-06T23:08:17.353 回答
1

这是我过去使用的方法(也许正如 Aaron 的评论所暗示的那样)。请注意,在 C 和 C++ 中使用相同的类型名称。强制转换都是用 C++ 完成的;无论合法性问题如何,这自然代表了良好的封装。[显然你也需要delete方法。] 请注意,要someOperation()使用 a调用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();
}
于 2012-02-07T00:35:25.307 回答
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 代码将始终严格处理实际上是 a 的值的Base*策略 - 这是确保一致性的有点武断的策略。这意味着 C++ 代码有时必须使用 dynamic_cast,我们稍后会谈到。

(正如其他人所提到的,您可以简单地通过使用强制转换使设计正确地使用 C 代码。但我会担心编译器会允许各种疯狂的强制转换,例如(Derived1*) derived2_ptr甚至强制转换为 a不同的类层次结构。我的目标是在 C 代码中强制执行正确的面向对象的is-a关系。)

然后,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这将是非常安全的类型,因为如果不通过数据成员,您将无法将 derived1_t_ptr 传递给该函数。它还将帮助您的 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();
}
于 2012-02-07T00:32:50.827 回答