reales - La mejor manera de declarar una interfaz en C++ 11
libro de android studio en español pdf (5)
Como todos sabemos, algunos idiomas tienen la noción de interfaces. Esto es Java:
public interface Testable {
void test();
}
¿Cómo puedo lograr esto en C ++ (o C ++ 11) de la forma más compacta y con poco ruido de código? Apreciaría una solución que no necesitaría una definición separada (deje que el encabezado sea suficiente). Este es un enfoque muy simple que incluso encuentro buggy ;-)
class Testable {
public:
virtual void test() = 0;
protected:
Testable();
Testable(const Testable& that);
Testable& operator= (const Testable& that);
virtual ~Testable();
}
Esto es solo el comienzo ... y ya no querría más. ¿Cómo mejorarlo? ¿Quizás hay una clase base en algún lugar del espacio de nombres estándar hecho solo para esto?
Al reemplazar la class
palabras con struct
, todos los métodos serán públicos de forma predeterminada y puede guardar una línea.
No hay necesidad de proteger al constructor, ya que de todos modos no se puede crear una instancia de una clase con métodos virtuales puros. Esto va también para el constructor de copia. El constructor predeterminado generado por el compilador estará vacío ya que no tiene ningún miembro de datos, y es completamente suficiente para sus clases derivadas.
Tienes razón en preocuparte por el operador =
ya que el generador generado por el compilador hará lo incorrecto. En la práctica, nadie se preocupa por eso porque copiar un objeto de interfaz a otro nunca tiene sentido; No es un error que sucede comúnmente.
Los destructores para una clase heredable siempre deben ser públicos y virtuales, o protegidos y no virtuales. Prefiero público y virtual en este caso.
El resultado final es solo una línea más larga que el equivalente de Java:
struct Testable {
virtual void test() = 0;
virtual ~Testable();
};
De acuerdo con Scott Meyers (Effective Modern C ++): al declarar interfaz (o clase base polimórfica), se necesita un destructor virtual para obtener resultados adecuados de operaciones como delete
o typeid
en un objeto de clase derivado al que se accede a través de un puntero o referencia de clase base.
virtual ~Testable() = default;
Sin embargo, un destructor declarado por el usuario suprime la generación de las operaciones de movimiento, por lo que para admitir las operaciones de movimiento debe agregar:
Testable(Testable&&) = default;
Testable& operator=(Testable&&) = default;
La declaración de las operaciones de movimiento deshabilita las operaciones de copia y también necesita:
Testable(const Testable&) = default;
Testable& operator=(const Testable&) = default;
Y el resultado final es:
class Testable
{
public:
virtual ~Testable() = default; // make dtor virtual
Testable(Testable&&) = default; // support moving
Testable& operator=(Testable&&) = default;
Testable(const Testable&) = default; // support copying
Testable& operator=(const Testable&) = default;
virtual void test() = 0;
};
Para el polimorfismo dinámico (tiempo de ejecución), recomendaría el uso del lenguaje de interfaz no virtual (NVI). Este patrón mantiene la interfaz no virtual y pública, el destructor virtual y público, y la implementación puramente virtual y privada
class DynamicInterface
{
public:
// non-virtual interface
void fun() { do_fun(); } // equivalent to "this->do_fun()"
// enable deletion of a Derived* through a Base*
virtual ~DynamicInterface() = default;
private:
// pure virtual implementation
virtual void do_fun() = 0;
};
class DynamicImplementation
:
public DynamicInterface
{
private:
virtual void do_fun() { /* implementation here */ }
};
Lo bueno del polimorfismo dinámico es que puede, en tiempo de ejecución, pasar cualquier clase derivada donde se espera un puntero o referencia a la clase base de la interfaz. El sistema de tiempo de ejecución bajará automáticamente this
puntero de su tipo de base estática a su tipo derivado dinámico y llamará a la implementación correspondiente (por lo general, se realiza mediante tablas con punteros a funciones virtuales).
Para estática (polimorfismo en tiempo de compilación), recomendaría usar el Patrón de Plantilla Curiosamente Recurrente (CRTP). Esto es considerablemente más complicado porque la conversión descendente automática de base a derivado de polimporfismo dinámico se debe realizar con static_cast
. Esta conversión estática se puede definir en una clase auxiliar que cada interfaz estática deriva de
template<typename Derived>
class enable_down_cast
{
private:
typedef enable_down_cast Base;
public:
Derived const* self() const
{
// casting "down" the inheritance hierarchy
return static_cast<Derived const*>(this);
}
Derived* self()
{
return static_cast<Derived*>(this);
}
protected:
// disable deletion of Derived* through Base*
// enable deletion of Base* through Derived*
~enable_down_cast() = default; // C++11 only, use ~enable_down_cast() {} in C++98
};
A continuación, se define una interfaz estática como esta:
template<typename Impl>
class StaticInterface
:
// enable static polymorphism
public enable_down_cast< Impl >
{
private:
// dependent name now in scope
using enable_down_cast< Impl >::self;
public:
// interface
void fun() { self()->do_fun(); }
protected:
// disable deletion of Derived* through Base*
// enable deletion of Base* through Derived*
~StaticInterface() = default; // C++11 only, use ~IFooInterface() {} in C++98/03
};
y, finalmente, crea una implementación que se deriva de la interfaz consigo misma como parámetro.
class StaticImplementation
:
public StaticInterface< StaticImplementation >
{
private:
// implementation
friend class StaticInterface< StaticImplementation > ;
void do_fun() { /* your implementation here */ }
};
Esto todavía le permite tener múltiples implementaciones de la misma interfaz, pero necesita saber en el momento de la compilación qué implementación está llamando.
Entonces, ¿cuándo usar qué forma? Ambos formularios le permitirán reutilizar una interfaz común e inyectar pruebas de condición previa / posterior dentro de la clase de interfaz. La ventaja del polimorfismo dinámico es que tiene flexibilidad de tiempo de ejecución, pero paga por eso en las llamadas a funciones virtuales (normalmente una llamada a través de un puntero a función, con pocas oportunidades para integrarse). El polimporismo estático es el reflejo de eso: no hay sobrecarga de llamadas de función virtual, pero la desventaja es que necesita más código repetitivo y necesita saber qué está llamando en tiempo de compilación. Básicamente una compensación de eficiencia / flexibilidad.
NOTA: para el polimporismo en tiempo de compilación, también puede usar parámetros de plantilla. La diferencia entre la interfaz estática a través del lenguaje CRTP y los parámetros de plantilla ordinarios es que la interfaz de tipo CRTP es explícita (basada en funciones miembro), y la interfaz de plantilla está implícita (basada en expresiones válidas)
Qué pasa:
class Testable
{
public:
virtual ~Testable() { }
virtual void test() = 0;
}
En C ++ esto no tiene implicaciones sobre la posibilidad de copiar las clases secundarias. Todo lo que dice es que el niño debe implementar la test
(que es exactamente lo que desea para una interfaz). No puede crear una instancia de esta clase, por lo que no tiene que preocuparse por ningún constructor implícito, ya que nunca se puede llamar directamente como el tipo de interfaz principal.
Si desea imponer que las clases secundarias implementen un destructor, puede hacerlo también puro (pero todavía tiene que implementarlo en la interfaz).
También tenga en cuenta que si no necesita destrucción polimórfica, puede optar por hacer que su destructor protegido no sea virtual.
Tenga en cuenta que la "regla de tres" no es necesaria si no está administrando punteros, manejadores o todos los miembros de datos de la clase tienen sus propios destructores que administrarán cualquier limpieza. También en el caso de una clase base virtual, porque la clase base nunca puede ser instanciada directamente, no es necesario declarar un constructor si todo lo que desea es definir una interfaz que no tenga miembros de datos ... el compilador los valores por defecto están bien El único elemento que necesitaría mantener es el destructor virtual si planea llamar a delete
en un puntero del tipo de interfaz. Entonces, en realidad su interfaz puede ser tan simple como:
class Testable
{
public:
virtual void test() = 0;
virtual ~Testable();
}