Gestión de la herencia: final y delete

MQL5 permite imponer algunas restricciones a la herencia de clases y estructuras.

 

Palabra clave final

Añadiendo la palabra clave final después del nombre de la clase, el desarrollador puede desactivar la herencia de esa clase. Por ejemplo (FinalDelete.mq5):

class Base
{
};
 
class Derived final : public Base
{
};
 
class Concrete : public Derived // ERROR
{
};

El compilador devolverá el error «no se puede heredar de 'Derived' ya que ha sido declarado como 'final'».

Desgraciadamente, no hay consenso sobre las ventajas y las hipótesis de aplicación de tal restricción. La palabra clave permite a los usuarios de la clase saber que su autor, por una razón u otra, no recomienda tomarla como base (por ejemplo, su implementación actual es un borrador y cambiará mucho, lo que puede hacer que los posibles proyectos heredados dejen de compilar).

Algunas personas intentan fomentar el diseño de programas de esta manera, en la que se use la inclusión de objetos (composición) en lugar de la herencia. Una pasión excesiva por la herencia puede, en efecto, aumentar la cohesión de la clase (es decir, la influencia mutua), ya que todos los herederos pueden, de un modo u otro, modificar los datos o métodos padre (en particular, redefiniendo funciones virtuales). Como resultado, aumenta la complejidad de la lógica de funcionamiento del programa y la probabilidad de que se produzcan efectos secundarios imprevistos.

Una ventaja adicional del uso de final puede ser la optimización del código por parte del compilador: para punteros de tipos «finales», puede sustituir el envío dinámico de funciones virtuales por el estático.

 

Palabra clave delete

La palabra clave delete puede especificarse en el encabezado de un método para hacerlo inaccesible en la clase actual y sus descendientes. Los métodos virtuales de las clases padre no pueden eliminarse de esta forma (esto infringiría el «contrato» de la clase, es decir, los herederos dejarían de «ser» («es un») representantes de la misma clase).

class Base
{
public:
   void method() { Print(__FUNCSIG__); }
};
 
class Derived : public Base
{
public:
   void method() = delete;
};
 
void OnStart()
{
   Base *b;
   Derived d;
   
   b = &d;
   b.method();
   
   // ERROR:   
   // attempting to reference deleted function 'void Derived::method()'
   //    function 'void Derived::method()' was explicitly deleted
   d.method();
}

Si se intenta llamarlo, se producirá un error de compilación.

Vimos un error similar en la sección Conversión de tipos de objeto porque el compilador tiene cierta inteligencia para «eliminar» también métodos en ciertas condiciones.

Se recomienda marcar como eliminados los siguientes métodos para los que el compilador proporciona implementaciones implícitas:

  • constructor por defecto: Class(void) = delete;
  • constructor de copias: Class(const Class &object) = delete;
  • operador de copia/asignación: void operator=(const Class &object) = delete.

Si necesita alguno de ellos, debe definirlo explícitamente. En caso contrario, se considera una buena práctica abandonar la implementación implícita. La cuestión es que la implementación implícita es bastante sencilla y puede dar lugar a problemas difíciles de localizar, en particular, al convertir tipos de objetos.