Обтекание 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*, Это на один шаг меньше, чем было раньше. Обратите внимание на заказ:

  1. FirstDerived*
  2. Base* (через неявный static_cast<Base*>)
  3. void* (с помощью static_cast<void*>)
  4. first_derived_t* (с помощью reinterpret_cast<first_derived_t*>)
  5. base_t* (с помощью (base_t*)в стиле C++ reinterpret_cast<base_t*>)
  6. void* (через неявный static_cast<void*>)
  7. Base* (с помощью reinterpret_cast<Base*>)

Для звонков, которые обернуть FirstDerived Метод, вы получаете дополнительный бросок:

  1. 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();
}
Другие вопросы по тегам