2009-08-01 19 views
31

Dado que C++ carece de la característica interface de Java y C#, ¿cuál es la forma preferida de simular interfaces en clases de C++? Mi conjetura sería herencia múltiple de clases abstractas. ¿Cuáles son las implicaciones en términos de sobrecarga de memoria/rendimiento? ¿Existen convenciones de nomenclatura para tales interfaces simuladas, como SerializableInterface?¿Cómo puedo simular interfaces en C++?

+9

Falta la interfaz. Eso parece una fraseología negativa. A C++ no le faltan interfaces (la clase es una interfaz), simplemente carece de la interfaz de palabras clave porque no se ha retrasado. –

+3

La palabra clave Interface garantiza que no contiene código ni datos, por lo que interactuarán fácilmente con otras interfaces sin problemas. No hay forma de hacer tal garantía en C++, solo tienes que esperar que no hagan nada que entre en conflicto. Una gran cantidad de cosas que Java y C# hacen para la legibilidad del código, la interoperabilidad y la comprensibilidad fueron resueltas en respuesta a los problemas que las personas encontraban al trabajar en C++. –

+2

posible duplicado de [¿Cómo declaras una interfaz en C++?] (Http://stackoverflow.com/questions/318064/how-do-you-declare-an-interface-in-c) –

Respuesta

29

Puesto que C++ tiene herencia múltiple a diferencia de C# y Java, sí se puede hacer una serie de clases abstractas.

En cuanto a la convención, depende de usted; Sin embargo, me gusta preceder a los nombres de las clases con un I.

class IStringNotifier 
{ 
public: 
    virtual void sendMessage(std::string &strMessage) = 0; 
    virtual ~IStringNotifier() { } 
}; 

El rendimiento es nada de qué preocuparse en términos de comparación entre C# y Java. Básicamente, tendrá la sobrecarga de tener una tabla de búsqueda para sus funciones o una tabla virtual como cualquier herencia que haya dado con los métodos virtuales.

+5

Y crea la clase abstracta destructor virtual si desea eliminar el objeto utilizando un puntero a la clase abstracta. –

+0

@Jim Huang: acuerdo agregado para ser explícito. –

+9

@Michael Aaron Safyan: Gracias por la votación negativa, ¿tal vez sería mejor si juzgaste las preguntas por lo que están respondiendo, y no tu preferencia por el estilo de codificación? –

3

Las interfaces en C++ son clases que solo tienen funciones virtuales puras. P.ej. :

class ISerializable 
{ 
public: 
    virtual ~ISerializable() = 0; 
    virtual void serialize(stream& target) = 0; 
}; 

Esto no es una interfaz simulada, es una interfaz como los de Java, pero no lleva a los inconvenientes.

E.g. puede agregar métodos y miembros sin consecuencias negativas:

class ISerializable 
{ 
public: 
    virtual ~ISerializable() = 0; 
    virtual void serialize(stream& target) = 0; 
protected: 
    void serialize_atomic(int i, stream& t); 
    bool serialized; 
}; 

Para las convenciones de nombres ... no hay convenciones de nombres reales definidas en el lenguaje C++. Así que elige el que está en tu entorno.

la sobrecarga es 1 tabla estática y en las clases derivadas que todavía no tienen funciones virtuales, un puntero a la tabla estática.

+1

No creo que puedas tener constructores virtuales. Sin embargo, puedes tener destructores virtuales. – jkeys

+0

@hooked, error de tipeo corregido. – Christopher

+1

No veo ninguna razón para que el destructor sea puramente virtual, simplemente virtual sería suficiente. Además, declarar que dtor no es suficiente, tiene que definirse. En el caso de dtor virtual puro debe definirse fuera de la definición de clase, así: ISerializable :: ~ ISerializable() {} porque la gramática C++ no permite el especificador virtual puro y la función de miembro en clase definición. –

1

Si no se utiliza la herencia virtual, la sobrecarga no debería ser peor que la herencia regular con al menos una función virtual. Cada clase abstracta heredada agregará un puntero a cada objeto.

Sin embargo, si lo hace algo así como la optimización de clases base vacía, se puede minimizar que:

 
struct A 
{ 
    void func1() = 0; 
}; 

struct B: A 
{ 
    void func2() = 0; 
}; 

struct C: B 
{ 
    int i; 
}; 

El tamaño de C será de dos palabras.

7

"¿Cuáles son las implicaciones en términos de sobrecarga de memoria/rendimiento?"

Por lo general, ninguno excepto los de uso de llamadas virtuales en absoluto, aunque nada está garantizado tanto por la norma en términos de rendimiento.

En la sobrecarga de memoria, la optimización de la "clase base vacía" permite explícitamente al compilador diseñar estructuras tales que agregar una clase base que no tenga miembros de datos no aumente el tamaño de sus objetos. Creo que es poco probable que tenga que lidiar con un compilador que no hace esto, pero podría estar equivocado.

Adición de la primera función miembro virtual a una clase por lo general aumenta objetos por el tamaño de un puntero, en comparación con si no tuvieran funciones miembro virtuales. Agregar más funciones de miembros virtuales no hace ninguna diferencia adicional. Agregar clases base virtuales puede marcar una mayor diferencia, pero no necesita eso para lo que está hablando.

Agregar múltiples clases base con funciones miembro virtuales probablemente significa que en realidad solo obtendrá la optimización de la clase base vacía una vez, porque en una implementación típica el objeto necesitará múltiples punteros vtable. Entonces, si necesita múltiples interfaces en cada clase, puede agregar el tamaño de los objetos.

En cuanto a rendimiento, una llamada de función virtual tiene un poco más sobrecarga que una llamada a función no virtual, y más importante aún, puede suponer que generalmente (¿siempre?) No estará en línea. Agregar una clase base vacía generalmente no agrega ningún código a la construcción o destrucción, porque el constructor base vacío y el destructor se pueden insertar en el código constructor/destructor de la clase derivada.

Hay trucos que puede utilizar para evitar las funciones virtuales si desea que las interfaces explícitas, pero que no necesitan polimorfismo dinámico. Sin embargo, si estás tratando de emular Java, supongo que ese no es el caso.

código

Ejemplo:

#include <iostream> 

// A is an interface 
struct A { 
    virtual ~A() {}; 
    virtual int a(int) = 0; 
}; 

// B is an interface 
struct B { 
    virtual ~B() {}; 
    virtual int b(int) = 0; 
}; 

// C has no interfaces, but does have a virtual member function 
struct C { 
    ~C() {} 
    int c; 
    virtual int getc(int) { return c; } 
}; 

// D has one interface 
struct D : public A { 
    ~D() {} 
    int d; 
    int a(int) { return d; } 
}; 

// E has two interfaces 
struct E : public A, public B{ 
    ~E() {} 
    int e; 
    int a(int) { return e; } 
    int b(int) { return e; } 
}; 

int main() { 
    E e; D d; C c; 
    std::cout << "A : " << sizeof(A) << "\n"; 
    std::cout << "B : " << sizeof(B) << "\n"; 
    std::cout << "C : " << sizeof(C) << "\n"; 
    std::cout << "D : " << sizeof(D) << "\n"; 
    std::cout << "E : " << sizeof(E) << "\n"; 
} 

salida (GCC en una plataforma de 32 bits):

A : 4 
B : 4 
C : 8 
D : 8 
E : 12 
5

En realidad no hay necesidad de 'simular' nada, ya que no es que C++ no se encuentra nada que Java puede hacer con las interfaces.

Desde un puntero de C++ de vista, Java hace una disctinction "artificial" entre un interface y una class. Un interface es solo un class, todos sus métodos son abstractos y no pueden contener ningún miembro de datos.

Java hace esta restricción ya que no permite la herencia múltiple sin restricciones, pero sí permite class a implement interfaces múltiples.

En C++, un class es un class y un interface es un class. extends se logra por herencia pública y implements también se logra por herencia pública.

Heredar de múltiples clases de interfaz no pueden dar lugar a complicaciones adicionales, pero puede ser útil en algunas situaciones. Si te limitas a solo heredar clases de como máximo una clase que no sea de interfaz y cualquier cantidad de clases completamente abstractas, entonces no vas a encontrar otras dificultades de las que tendrías en Java (exceptuando otras diferencias de C++/Java).

En términos de memoria y gastos generales, si está recreando una jerarquía de clases de estilo Java, probablemente ya haya pagado el costo de la función virtual en sus clases en cualquier caso. Dado que de todos modos está utilizando diferentes entornos de tiempo de ejecución, no habrá ninguna diferencia fundamental en los gastos generales entre los dos en términos de costo de los diferentes modelos de herencia.

1

Por cierto MSVC 2008 tiene __interface palabra clave.

A Visual C++ interface can be defined as follows: 

- Can inherit from zero or more base 
    interfaces. 
- Cannot inherit from a base class. 
- Can only contain public, pure virtual 
    methods. 
- Cannot contain constructors, 
    destructors, or operators. 
- Cannot contain static methods. 
- Cannot contain data members; 
    properties are allowed. 

Esta característica es Microsoft específico. Precaución: __interface no tiene un destructor virtual que se requiera si elimina objetos por sus punteros de interfaz.

+0

Creo que esta es más la forma COM/DCOM de hacer C++. –

+0

¿Por qué crees que es así? –

+0

Solo porque son los únicos lugares donde lo he visto utilizado ;-), cuando se trabaja con interfaces COM. –

1

En C++ podemos ir más allá de las interfaces simples sin comportamiento de Java & co. Podemos agregar contratos explícitos (como en Diseño por contrato) con el patrón NVI.

struct Contract1 : noncopyable 
{ 
    virtual ~Contract1(); 
    Res f(Param p) { 
     assert(f_precondition(p) && "C1::f precondition failed"); 
     const Res r = do_f(p); 
     assert(f_postcondition(p,r) && "C1::f postcondition failed"); 
     return r; 
    } 
private: 
    virtual Res do_f(Param p) = 0; 
}; 

struct Concrete : virtual Contract1, virtual Contract2 
{ 
    ... 
}; 
0

No hay una buena manera de implementar una interfaz de la forma en que pregunta. El problema con un enfoque, como la clase base ISerializable completamente abstracta, radica en la forma en que C++ implementa la herencia múltiple. Considere lo siguiente:

class Base 
{ 
}; 
class ISerializable 
{ 
    public: 
    virtual string toSerial() = 0; 
    virtual void fromSerial(const string& s) = 0; 
}; 

class Subclass : public Base, public ISerializable 
{ 
}; 

void someFunc(fstream& out, const ISerializable& o) 
{ 
    out << o.toSerial(); 
} 

Es evidente que la intención es que la función toSerial() para serializar todos los miembros de la Subclase incluidos los que hereda de la clase base. El problema es que no hay una ruta desde ISerializable a Base. Esto se puede ver gráficamente si se ejecuta el siguiente:

void fn(Base& b) 
{ 
    cout << (void*)&b << endl; 
} 
void fn(ISerializable& i) 
{ 
    cout << (void*)&i << endl; 
} 

void someFunc(Subclass& s) 
{ 
    fn(s); 
    fn(s); 
} 

El valor de salida de la primera llamada no es el mismo que el valor de salida de la segunda llamada. Aunque se pasa una referencia a s en ambos casos, el compilador ajusta la dirección pasada para que coincida con el tipo de clase base adecuado.