2012-02-06 9 views
5

Zawijam prostą hierarchię dziedziczenia C++ do "obiektowo zorientowanego" C. Próbuję ustalić, czy istnieją jakieś błędy w traktowaniu wskaźników do obiektów C++ jako wskaźników nieprzejrzystych C structs. W szczególności, w jakich okolicznościach konwersja wyprowadzona z bazy powodowałaby problemy?Zawijanie C++ w C: pochodne do bazowych konwersji

Klasy sami są stosunkowo skomplikowane, ale hierarchia jest płytkie i wykorzystuje tylko jeden-dziedziczenia:

// 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.. 

Ja planuje na wystawienie tego klientom C poprzez następujący, dość standardowe obiektowego 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; 
} 

Klienty C przekazują tylko wskaźniki do obiektów C++ i mogą nimi manipulować tylko za pomocą wywołań funkcji. Tak więc klient może w końcu zrobić coś takiego:

first_derived_t* object = first_derived_create(); 
base_some_operation((base_t*) object); // Note the derived-to-base cast here 
... 

i mieć wirtualny wezwanie do FirstDerived :: someOperation() sukcesu, jak oczekiwano.

Te klasy nie są standard-layout, ale nie korzystają z dziedziczenia wielokrotnego lub wirtualnego. Czy to gwarantuje działanie?

Należy pamiętać, że mam kontrolę nad całym kodem (C++ i C wrapper), jeśli to ma znaczenie.

+1

Po prostu używaj 'void *' wszędzie jako nieprzezroczystego typu. –

Odpowiedz

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

Oznacza to, że zawsze można wykonać odlew takie jak poniżej.

first_derived_t* object = first_derived_create(); 
base_some_operation((base_t*) object); 

Jest to bezpieczne, ponieważ wskaźnik base_t* zostaną oddane do void*, następnie do Base*. To o krok mniej niż wcześniej.Zwróć uwagę na kolejność:

  1. FirstDerived*
  2. Base* (poprzez ukryte static_cast<Base*>)
  3. void* (poprzez static_cast<void*>)
  4. first_derived_t* (poprzez reinterpret_cast<first_derived_t*>)
  5. base_t* (poprzez (base_t*), który jest C++ - stylu reinterpret_cast<base_t*>)
  6. void* (poprzez ukryte static_cast<void*>)
  7. Base* (poprzez reinterpret_cast<Base*>)

przypadku połączeń, które zawinąć metodę FirstDerived, otrzymasz dodatkowy Obsada:

  1. FirstDerived* (poprzez dynamic_cast<FirstDerived*>)
+0

Przyjemnie, to wydaje się być zarówno bezpieczne, jak i czyste, nawet w obliczu przyszłych programistów, którzy mogą nie zaszkodzić całej sytuacji od końca do końca. Dzięki! – Adrian

4

Z pewnością można utworzyć interfejs C do kodu C++. Wszystko czego potrzebujesz to extern "C" i polecam void * jako nieprzezroczyste typ danych:

// 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 *); 

następnie wdrożyć go w 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; 
    } 
} 

Pamiętaj, aby nigdy nie pozwolić wszelkie wyjątki propagować poprzez powołanie Funkcja C!

+2

Zrobiłem to wcześniej, a użycie 'void *' jest naprawdę problematyczne ze względów bezpieczeństwa typów po stronie klienta (łatwo jest przekazać * cokolwiek * jako argument). Rozważmy interfejs API systemu Windows, w którym pliki i obiekty mutex są identyfikowane przez typ 'HANDLE', co oznacza, że ​​można przekazać plik do' ReleaseMutex() '. Możesz użyć niekompletnej struktury dla każdego odrębnego typu, który powinien pojawić się po stronie klienta. Na przykład 'struct foo_t; typedef struct foo_t * foo_t; '. Następnie użyj 'foo_t create_foo();' i 'void destroy_foo (foo_t);'. –

+0

Dodaje kilka nieprzyjemnych rzutów wewnątrz implementacji kodu C++-to-C, ale przynajmniej uzyskujesz interfejs "bezpieczny dla typu" po stronie klienta. –

+0

@Kerrek SB: Dzięki za odpowiedź, ale jest to prostsze niż w moim przypadku. Martwię się o obsadę od FirstDerived * do Base * poprzez obsadę w stylu C. Na przykład, jeśli w grę wchodziło wiele dziedzin, mielibyśmy kłopoty, nie? Szukam innych gotch ... – Adrian

0

Myślę, że te dwie linie są sedno pytania:

first_derived_t* object = first_derived_create(); 
base_some_operation((base_t*) object); // Note the derived-to-base cast here 
... 

Nie ma naprawdę bezpieczny sposób pozwala na to w kodzie C. W C, taka obsada nigdy nie zmienia surowej wartości integralnej wskaźnika, ale czasami odlejeny C++ zrobi to i dlatego potrzebujesz projektu, który nigdy nie ma rzutów w kodzie C.

Oto jedno (zbyt skomplikowane?) Rozwiązanie. Po pierwsze, zdecyduj o polityce, że kod C zawsze będzie ściśle dotyczył wartości, która jest faktycznie Base* - jest to polityka nieco arbitralna, aby zapewnić spójność. Oznacza to, że kod C++ będzie musiał czasami przejść do dynamic_cast, dojdziemy do tego później.

(Możesz sprawić, by projekt działał poprawnie z kodem C, używając po prostu rzutów, o czym już wspominali inni, ale martwiłbym się, że kompilator pozwoli na różnego rodzaju szalone rzuty, takie jak (Derived1*) derived2_ptr lub nawet rzuca na typy w innej hierarchii klas. Moim celem jest tutaj wymuszenie odpowiedniej relacji obiektowej is-a w kodzie C.)

Następnie, Klasy C uchwyt może być coś podobnego

struct base_t_ptr { 
    void * this_; // holds the Base pointer 
}; 
typedef struct { 
    struct base_t_ptr get_base; 
} derived_t_ptr; 

Powinno to łatwy w użyciu coś podobnego odlewów w zwięzły i bezpieczny sposób: Uwaga jak mijamy w object.get_base w tym kodzie:

first_derived_t_ptr object = first_derived_create(); 
base_some_operation(object.get_base); 

gdzie deklaracja base_some_operation jest

extern "C" base_some_operation(struct base_t_ptr); 

To będzie dość bezpieczny typ, ponieważ nie będzie można przekazać funkcji pochodnej1_t_ptr do tej funkcji bez przechodzenia przez element danych .get_base. Pomoże to również kodowi C dowiedzieć się trochę o typach i jakie konwersje są prawidłowe - nie chcesz przypadkowo przekonwertować Derived1 na Derived2.

Następnie przy wdrażaniu nie-wirtualnych metody zdefiniowane tylko w klasie pochodnej, trzeba coś takiego:

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(); 
} 
1

Jest to podejście Użyłem w przeszłości (być może jako sugerowane przez komentarz Aarona). Zauważ, że nazwy typu same są używane zarówno w C jak i C++. Obsady są wykonywane w C++; to naturalnie przedstawia dobre hermetyzowanie niezależnie od kwestii legalności. [Oczywiście potrzebne są również metody delete.] Pamiętaj, że aby zadzwonić pod numer someOperation() z Derived*, wymagane jest jawne uaktualnienie do Base*. JeśliDerived nie zapewnia żadnych nowych metod, takich jak someOtherOperation, nie trzeba wystawiać klientom klientów na adres Derived* i unikać rzutowania po stronie klienta.

plik nagłówka: "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 

Realizacja: "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(); 
}