domingo, 18 de octubre de 2009

Interfaces vs. Conceptos

¿Qué son los conceptos? Vamos a ver con un simple ejemplo, cómo podemos hacer una equivalencia entre las conocidas interfaces y los conceptos.

Imaginen esta "interfaz" (clase abstracta):
class IPortero {
public:
  virtual IPortero() { }
  virtual void ir_a_piso(int piso) = 0;
  virtual int piso_destino() = 0;
};
Tenemos otra clase Ascensor que podemos "personalizar" con nuestro propio portero, así nuestra implementación de portero puede hacer lo que se le de la gana:
class Ascensor {
  IPortero* m_portero;
public:
  Ascensor(IPortero* portero) {
    m_portero = portero;
  }
  void apretaron_boton_en_piso(int piso) {
    m_portero->ir_a_piso(piso);
    mover_ascensor(m_portero->piso_destino());
  }
  void mover_ascensor(int piso) { ... }
};
Con programación genérica, podemos reformular la interfaz convirtiéndola en un "concepto" y la clase Ascensor en una clase plantilla:
template<class TipoPortero>
class Ascensor {
  TipoPortero m_portero;
public:
  Ascensor() { }
  void apretaron_boton_en_piso(int piso) {
    m_portero.ir_a_piso(piso);
    mover_ascensor(m_portero.piso_destino());
  }
  void mover_ascensor(int piso) { ... }
};
La pregunta es, ¿qué demonios es TipoPortero?. La respuesta es sencilla: TipoPortero puede ser cualquier tipo de dato que cumpla los siguientes requisitos:
  • Tenga un constructor por omisión (que se pueda construir un nuevo TipoPortero sin argumentos, o sea, TipoPortero()).
  • Tenga una función miembro TipoPortero::ir_a_piso(int), la cual recibe un "int" (o cualquier tipo de dato que se pueda construir desde un "int" implícitamente).
  • Y otra función miembro TipoPortero::piso_destino() que devuelve un entero.

¿Cómo especificamos la "interfaz" o los "requerimientos" de un concepto? Sencillamente no se puede, C++0x iba a soportar esto, pero ya no. Hoy en día la mejor respuesta es usar algunos comentarios en la clase Ascensor que especifiquen qué espera en sus parámetros de template. En este aspecto se podría decir que IPortero es mejor porque especifica explícitamente lo que el portero tiene que hacer (funciones a implementar, etc.).

¿Qué ventaja tiene el concepto con respecto a las interfaces? La clase genérica Ascensor ahora tiene el mismo portero adentro suyo (no un puntero a la interfaz). Las llamadas a las funciones miembro ir_a_piso y piso_destino son llamadas directas (no tienen el overhead de una llamada a una función virtual).

sábado, 17 de octubre de 2009

Clases que "desaparecen" luego de compilar

La magia de C++ es que, una vez compilado el código, algunas clases pueden desaparecer por completo (principalmente las que se usan en stack). O sea, aunque las clases abstraen al programador de los detalles de implementación, al final, el código termina siendo tan óptimo como si la clase no fuera utilizada en un principio.

Un ejemplo. Teniendo la siguiente clase Acumulador:
#include <cstdio>

class Acumulador {
  int v;
public:
  Acumulador()         { v = 0;                   }
  ~Acumulador()        { std::printf("%d\n", v);  }
  void acumular(int x) { v += x;                  }
};
Un código como el siguiente:
{
  Acumulador acum;
  acum.acumular(2);
  acum.acumular(4);
  acum.acumular(10);
}
Al compilarlo (optimizándolo), el código equivale a exactamente esto:
{
  int v = 0;
  v += 2;
  v += 4;
  v += 10;
  std::printf("%d\n", v);
}
La clase Acumulador ya no existe. Obtenemos el código más óptimo posible: sin llamadas a la función "acumular", ni ningún byte extra de memoria (Acumulador ocupa lo mismo de memoria que ocupa un "int").

Este ejemplo no ayuda a ver grandes ventajas, pero si el constructor y el destructor hacen tareas complicadas, y las funciones miembros también, el resultado puede llevarnos a dos puntos:
  • Nos abstrae de la complejidad de la implementación (e.j. para qué quiero saber cómo se acumula si sólo quiero acumular)
  • Obtenemos código tan óptimo como si no hubiéramos usado la abstracción (e.j. las operaciones se acercan al hardware tanto como sea posible).