¿Cómo puedo simular interfaces en C++?
naming-conventions abstract-class (8)
Como C ++ carece de la función de 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? ¿Hay alguna convención de nombres para tales interfaces simuladas, como SerializableInterface
?
"¿Cuáles son las implicaciones en términos de sobrecarga de memoria / rendimiento?"
Por lo general, ninguno excepto el uso de llamadas virtuales en absoluto, aunque el estándar no garantiza mucho 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 de manera 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.
Agregar la primera función de miembro virtual a una clase generalmente aumenta los objetos por el tamaño de un puntero, en comparación con si no tenían funciones de miembros 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 al rendimiento, una llamada de función virtual tiene un poco más de sobrecarga que una llamada de función no virtual, y lo más importante, 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 usar para evitar funciones virtuales si desea interfaces explícitas, pero no necesita polimorfismo dinámico. Sin embargo, si estás tratando de emular Java, supongo que ese no es el caso.
Código de 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
Dado que C ++ tiene herencia múltiple a diferencia de C # y Java, sí 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 clase con un I.
class IStringNotifier
{
public:
virtual void sendMessage(std::string &strMessage) = 0;
virtual ~IStringNotifier() { }
};
El rendimiento no 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.
En C ++ podemos ir más allá de las interfaces simples sin comportamiento de Java & co. Podemos agregar contratos explícitos (como en Design by Contract ) 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
{
...
};
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;
};
Esta no es una interfaz simulada, es una interfaz como las de Java, pero no tiene los inconvenientes.
Por ejemplo, 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 aún no tienen funciones virtuales, un puntero a la tabla estática.
No hay una buena manera de implementar una interfaz de la forma en que está preguntando. El problema con un enfoque, como la clase base ISerializable completamente abstracta, radica en la forma en que C ++ implementa la herencia múltiple. Considera 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();
}
Claramente, la intención es que la función aSerial () serialice todos los miembros de la Subclase, incluidos aquellos que hereda de la clase Base. El problema es que no hay una ruta desde ISerializable a Base. Puede ver esto gráficamente si ejecuta lo 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 emitido por la primera llamada no es el mismo que el valor emitido por 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.
Por cierto, MSVC 2008 tiene __interface keyword.
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 específica de Microsoft. Precaución: __interface no tiene un destructor virtual que sea necesario si elimina objetos mediante sus punteros de interfaz.
Realmente no hay necesidad de ''simular'' nada, ya que C ++ no tiene nada que Java pueda hacer con las interfaces.
Desde un puntero de vista C ++, Java realiza una disctinción "artificial" entre una interface
y una class
. Una interface
es solo una class
cuyos métodos son abstractos y no pueden contener ningún miembro de datos.
Java establece esta restricción ya que no permite la herencia múltiple sin restricciones, pero sí permite que una class
implement
múltiples interfaces.
En C ++, una class
es una class
y una interface
es una class
. extends
se logra mediante la herencia pública y los implements
también se logra por herencia pública.
Heredar de múltiples clases que no son de interfaz puede provocar 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 costos 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.
Si no usa herencia virtual, la sobrecarga no debería ser peor que la herencia normal con al menos una función virtual. Cada clase abstracta heredada agregará un puntero a cada objeto.
Sin embargo, si hace algo como la Optimización de clase base vacía, puede minimizar eso:
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.