Plantillas de métodos

No sólo un tipo de objeto en su conjunto puede ser una plantilla, sino que su método por separado, simple o estático, también puede ser una plantilla. La excepción son los métodos virtuales: estos no pueden convertirse en plantillas. De ello se deduce que los métodos de plantilla no pueden declararse dentro de interfaces. No obstante, las propias interfaces pueden convertirse en plantillas, y los métodos virtuales pueden estar presentes en las plantillas de las clases.

Cuando una plantilla de método está contenida dentro de una plantilla de clase o estructura, los parámetros de ambas plantillas deben ser diferentes. Si hay varios métodos de plantilla, sus parámetros no están relacionados de ninguna manera y pueden tener el mismo nombre.

Una plantilla de método se declara de forma similar a una plantilla de funciones, pero sólo en el contexto de una clase, estructura o unión (que pueden o no ser plantillas).

template < typename T ⌠, typename Ti ...] > ]
class class_name
{
   ...
   template < typename U [, typename Ui ...] >
  type method_name(parameters_with_types_T_and_U)
   {
   }
};

Los parámetros, el valor de retorno y el cuerpo del método pueden utilizar los tipos T (general para una clase) y U (específico para un método).

Una instancia de un método para una combinación específica de parámetros se genera sólo cuando es invocada en el código del programa.

En la sección anterior describimos la clase de plantilla AutoPtr para almacenar y liberar un único puntero. Cuando hay muchos punteros del mismo tipo es conveniente ponerlos en un objeto contenedor. Vamos a crear una plantilla simple con una funcionalidad similar: la clase SimpleArray (SimpleArray.mqh). Con el fin de no duplicar la funcionalidad para controlar la liberación de memoria dinámica, pondremos en el contrato de la clase que está pensada para almacenar valores y objetos, pero no punteros. Para almacenar los punteros, los colocaremos en objetos AutoPtr, y éstos en el contenedor.

Esto tiene otro efecto positivo: como el objeto AutoPtr es pequeño, es fácil de copiar (sin gastar demasiados recursos en ello), lo que suele ocurrir cuando se intercambian datos entre funciones. Los objetos de esas clases de aplicación a los que apunta AutoPtr pueden ser grandes, y ni siquiera es necesario implementar en ellos su propio constructor de copia.

Por supuesto, es más barato devolver punteros desde las funciones, pero entonces hay que reinventar los medios de control de liberación de memoria. Por lo tanto, es más fácil utilizar una solución ya preparada en forma de AutoPtr.

Para los objetos de dentro del contenedor, crearemos el array data del tipo con plantilla T.

template<typename T>
class SimpleArray
{
protected:
   T data[];
   ...

Dado que una de las principales operaciones de un contenedor es añadir un elemento, vamos a proporcionar una función de ayuda para ampliar el array.

   int expand()
   {
      const int n = ArraySize(data);
      ArrayResize(datan + 1);
      return n;
   }

Añadiremos elementos directamente a través del operador sobrecargado '<<'. Utiliza el parámetro genérico de plantilla T.

public:
   SimpleArray *operator<<(const T &r)
   {
      data[expand()] = (T)r;
      return &this;
   }

Esta opción toma un valor por referencia, es decir, una variable o un objeto. Por ahora, debe prestar atención a esto, y en unos instantes se verá por qué es importante.

La lectura de elementos se realiza sobrecargando el operador '[]' (tiene la precedencia más alta y, por tanto, no requiere el uso de paréntesis en las expresiones).

   T operator[](int iconst
   {
      return data[i];
   }

Primero, asegurémonos de que la clase funciona en el ejemplo de la estructura.

struct Properties
{
   int x;
   string s;
};

Para ello, describiremos un contenedor para la estructura en la función OnStart y colocaremos en él un objeto (TemplatesSimpleArray.mq5).

void OnStart()
{
   SimpleArray<PropertiesarrayStructs;
   Properties prop = {12345"abc"};
   arrayStructs << prop;
   Print(arrayStructs[0].x" "arrayStructs[0].s);
   ...
}

El registro de depuración permite verificar que la estructura se encuentra en un contenedor.

Ahora vamos a intentar almacenar algunos números en el contenedor.

   SimpleArray<doublearrayNumbers;
   arrayNumbers << 1.0 << 2.0 << 3.0;

Por desgracia, obtendremos errores de «parámetro pasado como referencia, variable esperada», que se producen exactamente en el operador sobrecargado '<<'.

Necesitamos una sobrecarga con paso de parámetros por valor. Sin embargo, no podemos escribir simplemente un método similar que no tenga const y '&':

   SimpleArray *operator<<(T r)
   {
      data[expand()] = (T)r;
      return &this;
   }

Si lo hace, la nueva variante dará lugar a una plantilla no compilable para tipos de objeto: al fin y al cabo, los objetos sólo deben pasarse por referencia. Aunque la función no se utilice para objetos, sigue estando presente en la clase. Por lo tanto, definiremos el nuevo método como una plantilla con su propio parámetro.

template<typename T>
class SimpleArray
{
   ...
   template<typename U>
   SimpleArray *operator<<(U u)
   {
      data[expand()] = (T)u;
      return &this;
   }

Aparecerá en la clase sólo si se pasa algo por valor al operador '<<', lo que significa que definitivamente no es un objeto. Es cierto que no podemos garantizar que T y U sean iguales, así que se realiza una conversión explícita (T)u. Para los tipos integrados (si los dos tipos no coinciden), en algunas combinaciones, la conversión con pérdida de precisión es posible, pero el código se compilará con seguridad. La única excepción es la prohibición de convertir una cadena en un tipo booleano, pero es poco probable que el contenedor se utilice para el array bool, por lo que esta restricción no es significativa. Quienes lo deseen pueden resolver este problema.

Con el nuevo método de plantilla, el contenedor SimpleArray<double> funciona como se esperaba y no entra en conflicto con SimpleArray<Properties> porque las dos instancias de plantilla tienen diferencias en el código fuente generado.

Por último, vamos a comprobar el contenedor con objetos AutoPtr. Para ello, prepararemos una clase sencilla Dummy que «suministrará» objetos para punteros dentro de AutoPtr.

class Dummy
{
   int x;
public:
   Dummy(int i) : x(i) { }
   int value() const
   {
      return x;
   }
};

Dentro de la función OnStart vamos a crear un contenedor SimpleArray<AutoPtr<Dummy>> y a llenarlo.

void OnStart()
{
   SimpleArray<AutoPtr<Dummy>> arrayObjects;
   AutoPtr<Dummyptr = new Dummy(20);
   arrayObjects << ptr;
   arrayObjects << AutoPtr<Dummy>(new Dummy(30));
   Print(arrayObjects[0][].value());
   Print(arrayObjects[1][].value());
}

Recuerde que en AutoPtr se utiliza el operador '[]' para devolver un puntero almacenado, por lo que arrayObjects[0][] significa: devolver el elemento 0 del array data en SimpleArray, es decir, el objeto AutoPtr, y a continuación se aplica el segundo par de corchetes al volumen, lo que da lugar a un puntero Dummy*. Seguidamente, podemos trabajar con todas las propiedades de este objeto: en este caso, recuperamos el valor actual del campo x.

Dado que Dummy no dispone de un constructor de copia, no puede utilizar un contenedor para almacenar estos objetos directamente sin AutoPtr.

   // ERROR:
   // object of 'Dummy' cannot be returned,
   // copy constructor 'Dummy::Dummy(const Dummy &)' not found
   SimpleArray<Dummybad;

No obstante, un usuario ingenioso puede averiguar cómo evitarlo.

   SimpleArray<Dummy*> bad;
   bad << new Dummy(0);

Este código se compilará y ejecutará, pero esta «solución» encierra un problema: SimpleArray no sabe controlar los punteros y, por lo tanto, cuando el programa sale, se detecta una fuga de memoria.

1 undeleted objects left
1 object of type Dummy left
24 bytes of leaked memory

Nosotros, como desarrolladores de SimpleArray, tenemos el deber de llenar ese vacío. Para ello, vamos a añadir otro método de plantilla a la clase con una sobrecarga del operador '<<', esta vez para punteros. Dado que se trata de una plantilla, sólo se incluye en el código fuente resultante «a demanda»: cuando el programador intenta utilizar esta sobrecarga, es decir, escribir un puntero al contenedor. En caso contrario, el método se ignora.

template<typename T>
class SimpleArray
{
   ...
   template<typename P>
   SimpleArray *operator<<(P *p)
   {
      data[expand()] = (T)*p;
      if(CheckPointer(p) == POINTER_DYNAMICdelete p;
      return &this;
   }

Esta especialización arroja un error de compilación («puntero de objeto esperado») al crear una instancia de plantilla con un tipo de puntero. Por lo tanto, informamos al usuario de que este modo no es compatible.

   SimpleArray<Dummy*> bad// ERROR is generated in SimpleArray.mqh

Además, realiza otra acción protectora. Si la clase cliente todavía tiene un constructor de copia, entonces guardar objetos asignados dinámicamente en el contenedor ya no provocará una fuga de memoria: una copia del objeto en el puntero pasado P *p permanece en el contenedor, y el original se elimina. Cuando el contenedor se destruye al final de la función OnStart, su array interno data llamará automáticamente a los destructores para sus elementos.

void OnStart()
{
   ...
   SimpleArray<Dummygood;
   good << new Dummy(0);
// SimpleArray "cleans" its elements
 // no forgotten objects in memory

Las plantillas de métodos y los métodos «simples» pueden definirse fuera del bloque de la clase principal (o plantilla de clase), de forma similar a lo que vimos en el método Dividir declaración y definición de clase. Al mismo tiempo, todas van precedidas por el encabezado de la plantilla (TemplatesExtended.mq5):

template<typename T>
class ClassType
{
   ClassType() // private constructor
   {
      s = &this;
   }
   static ClassType *s// object pointer (if it was created)
public:
   static ClassType *create() // creation (on first call only)
   {
      static ClassType single//single pattern for every T
      return single;
   }
 
   static ClassType *check() // checking pointer without creating
   {
      return s;
   }
   
   template<typename U>
   void method(const U &u);
};
   
template<typename T>
template<typename U>
void ClassType::method(const U &u)
{
   Print(__FUNCSIG__" "typename(T), " "typename(U));
}
   
template<typename T>
static ClassType<T> *ClassType::s = NULL;

También muestra la inicialización de una variable estática con plantilla, lo que denota el patrón de diseño singleton.

En la función OnStart, cree una instancia de la plantilla y pruébela:

void OnStart()
{
   ClassType<string> *object = ClassType<string>::create();
   double d = 5.0;
   object.method(d);
   // OUTPUT:
   // void ClassType<string>::method<double>(const double&) string double
   
   Print(ClassType<string>::check()); // 1048576 (an example of an instance id)
    Print(ClassType<long>::check());   // 0 (there is no instance for T=long)
}