2012-02-06 18 views
5

Estoy envolviendo una jerarquía simple de herencia C++ en "orientada a objetos" C. Estoy tratando de averiguar si hay errores al tratar los punteros a objetos C++ como punteros a opacos C construye. En particular, ¿bajo qué circunstancias la conversión derivada a la base causaría problemas?Envoltura C++ en C: derivadas a conversiones base

Las clases mismas son relativamente complejos, pero la jerarquía es poco profunda y utiliza una sola herencia única:

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

Estoy pensando en la exposición de este a clientes de C a través de la siguiente, bastante estándar orientado a objetos 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; 
} 

los clientes de C solamente pasar alrededor de punteros a los objetos subyacentes de C++ y sólo puede manipularlos a través de llamadas a funciones. Así el cliente puede finalmente hacer algo como:

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

y tienen la llamada virtual a FirstDerived :: someOperation() tener éxito como se esperaba.

Estas clases no son standard-layout pero no usan herencia múltiple o virtual. ¿Esto está garantizado para funcionar?

Tenga en cuenta que tengo control sobre todo el código (C++ y el contenedor C), si eso es importante.

+1

Simplemente use 'void *' en todas partes como su tipo opaco. –

Respuesta

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

Esto significa que siempre se puede llevar a cabo un reparto como el siguiente.

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

Esto es seguro porque el puntero base_t* será echado a void*, a continuación, a Base*. Esto es un paso menos de lo que sucedió antes.Observe el orden:

  1. FirstDerived*
  2. Base* (vía implícita static_cast<Base*>)
  3. void* (a través de static_cast<void*>)
  4. first_derived_t* (a través de reinterpret_cast<first_derived_t*>)
  5. base_t* (a través de (base_t*), que es un C++ - estilo reinterpret_cast<base_t*>)
  6. void* (vía implícita static_cast<void*>)
  7. Base* (a través de reinterpret_cast<Base*>)

Para las llamadas que envuelven un método FirstDerived, se obtiene un reparto adicional:

  1. FirstDerived* (a través de dynamic_cast<FirstDerived*>)
+0

Bueno, esto parece ser seguro y limpio, incluso frente a futuros programadores de mantenimiento que podrían no asimilar toda la situación de extremo a extremo. ¡Gracias! – Adrian

4

Sin duda puede hacer una interfaz C para algunos códigos C++. Todo lo que necesita es extern "C", y recomiendo un void * como su opaca tipo de datos:

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

luego implementar en 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; 
    } 
} 

Recuerde nunca para permitir cualquier excepciones se propaguen a través de una llamada Función C!

+2

He hecho esto antes, y usar 'void *' es realmente problemático por razones de seguridad de tipo en el lado del cliente (es fácil pasar * cualquier cosa como ese argumento). Considere la API de Windows donde los archivos y los objetos mutex se identifican por el tipo 'HANDLE', lo que significa que puede pasar un archivo a' ReleaseMutex() '. Puede usar una estructura incompleta para cada tipo diferenciado que debería aparecer en el lado del cliente. Por ejemplo, 'struct foo_t; typedef struct foo_t * foo_t; '. Luego, use 'foo_t create_foo();' y 'void destroy_foo (foo_t);'. –

+0

Añade algunos moldes desagradables dentro de la implementación del apéndice C++ - to-C, pero al menos se obtiene una interfaz "tipo segura" en el lado del cliente. –

+0

@Kerrek SB: Gracias por la respuesta, pero esto es más simple que mi caso. Me preocupa el reparto de FirstDerived * a Base * a través de un elenco de estilo C. Por ejemplo, si hubiera herencia múltiple involucrada, estaríamos en problemas, ¿no? Estoy buscando otros problemas ... – Adrian

0

Creo que estas dos líneas son el quid de la cuestión:

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

No hay manera realmente segura para permitir que esta en el código C. En C, dicho reparto nunca cambia el valor integral en bruto del puntero, pero a veces los moldes C++ lo harán y, por lo tanto, necesita un diseño que nunca tenga moldes dentro del código C.

Aquí hay una solución (¿demasiado compleja?). Primero, decida sobre una política que el código C siempre tratará estrictamente con un valor que efectivamente es un Base*; esta es una política algo arbitraria para garantizar la coherencia. Esto significa que el código C++ a veces tendrá que ser un dynamic_cast, lo veremos más adelante.

(Puede hacer que el diseño funcione correctamente con el código C simplemente usando moldes, como ha sido mencionado por otros. Pero me preocuparía que el compilador permita todo tipo de moldes locos, como (Derived1*) derived2_ptr o incluso arroja a tipos en una jerarquía de clases diferente. Mi objetivo aquí es hacer cumplir la relación orientada a objetos adecuada es una relación dentro del código C.)

A continuación, las clases de mango C podría ser algo así como

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

Esto debería hacer que sea fácil de usar algo así como moldes de una manera concisa y segura: Nota cómo se pasa en object.get_base en este código:

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

donde la declaración de base_some_operation es

extern "C" base_some_operation(struct base_t_ptr); 

Esto será bastante seguro, ya que no podrá pasar un derivado1_t_ptr a esta función sin pasar por el miembro de datos .get_base. También ayudará a su código C a conocer un poco sobre los tipos y qué conversiones son válidas; no desea convertir accidentalmente Derived1 en Derived2.

Entonces, al aplicar los no virtuales métodos definidos sólo en una clase derivada, necesitará algo como:

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

Este es el enfoque que he usado en el pasado (quizás como implicado por el comentario de Aaron). Tenga en cuenta que los mismos nombres de tipo se utilizan tanto en C como en C++. Los lanzamientos se hacen todos en C++; esto naturalmente representa una buena encapsulación independientemente de las cuestiones de legalidad. [Obviamente también necesita los métodos delete]. Tenga en cuenta que para llamar al someOperation() con un Derived*, se requiere un upcast explícito a Base*. SiDerived no proporciona ningún método nuevo como someOtherOperation, entonces no necesita exponer Derived* a los clientes, y evitar las versiones del lado del cliente.

Archivo de cabecera: "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 

Implementación: "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(); 
} 
Cuestiones relacionadas