Interfaces implícitas vs explícitas

9

Creo que entiendo las limitaciones reales del polimorfismo en tiempo de compilación y el polimorfismo en tiempo de ejecución. Pero, ¿cuáles son las diferencias conceptuales entre interfaces explícitas (polimorfismo en tiempo de ejecución, es decir, funciones virtuales y punteros / referencias) e interfaces implícitas (polimorfismo en tiempo de compilación, es decir, plantillas) .

Mi opinión es que dos objetos que ofrecen la misma interfaz explícita deben ser del mismo tipo de objeto (o tener un ancestro común), mientras que dos objetos que ofrecen la misma interfaz implícita no necesitan ser el mismo tipo de objeto y, excluyendo el implícito La interfaz que ambos ofrecen puede tener una funcionalidad bastante diferente.

Tiene alguna idea sobre esto?

Y si dos objetos ofrecen la misma interfaz implícita, qué razones (además del beneficio técnico de no necesitar un despacho dinámico con una tabla de búsqueda de funciones virtuales, etc.) están ahí para no tener estos objetos heredados de un objeto base que declara esa interfaz, por lo tanto convirtiéndolo en una interfaz explícita ? Otra forma de decirlo: ¿puede darme un caso en el que dos objetos que ofrecen la misma interfaz implícita (y, por lo tanto, se pueden usar como tipos para la clase de plantilla de muestra) no deberían heredar de una clase base que hace explícita esa interfaz?

Algunas publicaciones relacionadas:


Aquí hay un ejemplo para hacer esta pregunta más concreta:

Interfaz implícita

class Class1
{
public:
  void interfaceFunc();
  void otherFunc1();
};

class Class2
{
public:
  void interfaceFunc();
  void otherFunc2();
};

template <typename T>
class UseClass
{
public:
  void run(T & obj)
  {
    obj.interfaceFunc();
  }
};

Interfaz explícita

class InterfaceClass
{
public:
  virtual void interfaceFunc() = 0;
};

class Class1 : public InterfaceClass
{
public:
  virtual void interfaceFunc();
  void otherFunc1();
};

class Class2 : public InterfaceClass
{
public:
  virtual void interfaceFunc();
  void otherFunc2();
};

class UseClass
{
public:
  void run(InterfaceClass & obj)
  {
    obj.interfaceFunc();
  }
};

Un ejemplo aún más profundo y concreto:

Algunos problemas de C ++ se pueden resolver con:

  1. Una clase con plantilla cuyo tipo de plantilla proporciona una interfaz implícita
  2. una clase sin plantilla que toma un puntero de clase base que proporciona una interfaz explícita

Código que no cambia:

class CoolClass
{
public:
  virtual void doSomethingCool() = 0;
  virtual void worthless() = 0;
};

class CoolA : public CoolClass
{
public:
  virtual void doSomethingCool()
  { /* Do cool stuff that an A would do */ }

  virtual void worthless()
  { /* Worthless, but must be implemented */ }
};

class CoolB : public CoolClass
{
public:
  virtual void doSomethingCool()
  { /* Do cool stuff that a B would do */ }

  virtual void worthless()
  { /* Worthless, but must be implemented */ }
};

Caso 1 . Una clase sin plantilla que toma un puntero de clase base que proporciona una interfaz explícita:

class CoolClassUser
{
public:  
  void useCoolClass(CoolClass * coolClass)
  { coolClass.doSomethingCool(); }
};

int main()
{
  CoolA * c1 = new CoolClass;
  CoolB * c2 = new CoolClass;

  CoolClassUser user;
  user.useCoolClass(c1);
  user.useCoolClass(c2);

  return 0;
}

Caso 2 . Una clase con plantilla cuyo tipo de plantilla proporciona una interfaz implícita:

template <typename T>
class CoolClassUser
{
public:  
  void useCoolClass(T * coolClass)
  { coolClass->doSomethingCool(); }
};

int main()
{
  CoolA * c1 = new CoolClass;
  CoolB * c2 = new CoolClass;

  CoolClassUser<CoolClass> user;
  user.useCoolClass(c1);
  user.useCoolClass(c2);

  return 0;
}

Caso 3 . Una clase con plantilla cuyo tipo de plantilla proporciona una interfaz implícita (esta vez, no derivada de CoolClass:

class RandomClass
{
public:
  void doSomethingCool()
  { /* Do cool stuff that a RandomClass would do */ }

  // I don't have to implement worthless()! Na na na na na!
}


template <typename T>
class CoolClassUser
{
public:  
  void useCoolClass(T * coolClass)
  { coolClass->doSomethingCool(); }
};

int main()
{
  RandomClass * c1 = new RandomClass;
  RandomClass * c2 = new RandomClass;

  CoolClassUser<RandomClass> user;
  user.useCoolClass(c1);
  user.useCoolClass(c2);

  return 0;
}

El caso 1 requiere que el objeto que se pasa useCoolClass()sea ​​hijo de CoolClass(e implemente worthless()). Los casos 2 y 3, por otro lado, tomarán cualquier clase que tenga una doSomethingCool()función.

Si los usuarios del código siempre estuvieran bien subclasificados CoolClass, entonces el Caso 1 tiene un sentido intuitivo, ya CoolClassUserque siempre se esperaría una implementación de a CoolClass. Pero suponga que este código será parte de un marco de API, por lo que no puedo predecir si los usuarios querrán subclasificar CoolClasso rodar su propia clase que tiene una doSomethingCool()función.

Chris Morris
fuente
Tal vez me estoy perdiendo algo, pero ¿no es la diferencia importante ya expuesta sucintamente en su primer párrafo, que es que las interfaces explícitas son polimorfismo en tiempo de ejecución, mientras que las interfaces implícitas son polimorfismo en tiempo de compilación?
Robert Harvey
2
Hay algunos problemas que pueden resolverse teniendo una Clase o función que lleve un puntero a una Clase abstracta (que proporciona una interfaz explícita) o una Clase o función con plantilla que utilice un objeto que proporcione una interfaz implícita. Ambas soluciones funcionan. ¿Cuándo quieres usar la primera solución? ¿El segundo?
Chris Morris
Creo que la mayoría de estas consideraciones se desmoronan cuando abres los conceptos un poco más. por ejemplo, ¿dónde encajarías el polimorfismo estático sin herencia?
Javier

Respuestas:

8

Ya ha definido el punto importante: uno es el tiempo de ejecución y el otro es el tiempo de compilación . La información real que necesita son las ramificaciones de esta elección.

Tiempo de compilación:

  • Pro: las interfaces en tiempo de compilación son mucho más granulares que las en tiempo de ejecución. Con eso, lo que quiero decir es que puedes usar solo los requisitos de una sola función, o un conjunto de funciones, como las llamas. No tiene que hacer siempre toda la interfaz. Los requisitos son únicos y exactamente lo que necesita.
  • Pro: Técnicas como CRTP significan que puede usar interfaces implícitas para implementaciones predeterminadas de cosas como operadores. Nunca se podría hacer algo así con la herencia en tiempo de ejecución.
  • Pro: las interfaces implícitas son mucho más fáciles de componer y multiplicar "heredar" que las interfaces en tiempo de ejecución, y no imponen ningún tipo de restricciones binarias; por ejemplo, las clases POD pueden usar interfaces implícitas. No hay necesidad de virtualherencia u otras travesuras con interfaces implícitas, una gran ventaja.
  • Pro: El compilador puede hacer muchas más optimizaciones para las interfaces en tiempo de compilación. Además, el tipo de seguridad adicional hace que el código sea más seguro.
  • Pro: es imposible escribir valores para las interfaces en tiempo de ejecución, porque no conoce el tamaño o la alineación del objeto final. Esto significa que cualquier caso que necesite / se beneficie de la tipificación de valores obtiene grandes beneficios de las plantillas.
  • Contras: las plantillas son un poco complicadas de compilar y usar, y pueden ser complicadas de portar entre compiladores
  • Con: las plantillas no se pueden cargar en tiempo de ejecución (obviamente), por lo que tienen límites para expresar estructuras de datos dinámicas, por ejemplo.

Tiempo de ejecución:

  • Pro: El tipo final no tiene que decidirse hasta el tiempo de ejecución. Esto significa que la herencia en tiempo de ejecución puede expresar algunas estructuras de datos mucho más fácilmente, si las plantillas lo pueden hacer. También puede exportar tipos polimórficos en tiempo de ejecución a través de límites C, por ejemplo, COM.
  • Pro: es mucho más fácil especificar e implementar la herencia en tiempo de ejecución, y realmente no obtendrá ningún comportamiento específico del compilador.
  • Con: la herencia en tiempo de ejecución puede ser más lenta que la herencia en tiempo de compilación.
  • Con: la herencia en tiempo de ejecución pierde información de tipo.
  • Con: la herencia en tiempo de ejecución es mucho menos flexible.
  • Con: la herencia múltiple es una perra.

Dada la lista relativa, si no necesita una ventaja específica de la herencia en tiempo de ejecución, no la use. Es más lento, menos flexible y menos seguro que las plantillas.

Editar: Vale la pena señalar que en C ++ particularmente hay usos para la herencia que no sean el polimorfismo en tiempo de ejecución. Por ejemplo, puede heredar typedefs, o usarlo para el etiquetado de tipos, o usar el CRTP. Sin embargo, en última instancia, estas técnicas (y otras) realmente se incluyen en "Tiempo de compilación", a pesar de que se implementan utilizando class X : public Y.

DeadMG
fuente
Con respecto a su primer profesional para compilar, esto está relacionado con una de mis preguntas principales. ¿Alguna vez quisiera dejar en claro que solo desea trabajar con una interfaz explícita? Es decir. "No me importa si tienes todas las funciones que necesito, si no heredas de la Clase Z, no quiero tener nada que ver contigo". Además, la herencia en tiempo de ejecución no pierde información de tipo al usar punteros / referencias, ¿correcto?
Chris Morris
@ChrisMorris: No. Si funciona, entonces funciona, que es lo único que debería preocuparte. ¿Por qué hacer que alguien escriba exactamente el mismo código en otro lugar?
jmoreno
1
@ChrisMorris: No, no lo haría. Si solo necesito X, entonces es uno de los principios fundamentales básicos de encapsulación que solo debería pedir y preocuparme por X. Además, pierde información de tipo. No puede, por ejemplo, apilar un objeto de ese tipo. No puede crear una instancia de una plantilla con su tipo verdadero. No puede invocar funciones miembro con plantilla en ellos.
DeadMG
¿Qué pasa con una situación en la que tienes una clase Q que usa alguna clase? Q toma un parámetro de plantilla, por lo que cualquier clase que proporcione la interfaz implícita funcionará, o eso creemos. Resulta que la clase Q también espera que su clase interna (llámela H) use la interfaz de Q. Por ejemplo, cuando el objeto H se destruye, debería llamar a alguna función de Q's. Esto no se puede especificar en una interfaz implícita. Por lo tanto, las plantillas fallan. Dicho de manera más clara, un conjunto de clases estrechamente acoplado que requiere algo más que interfaces implícitas entre sí parece excluir el uso de plantillas.
Chris Morris
Con compiletime: Ugly to debug, necesidad de poner las definiciones en el encabezado
JFFIGK