Object type templates

An object type template definition begins with a header containing typed parameters (see section Template Header), and the usual definition of a class, structure, or union.

template <typename T [, typename Ti ...] >
class class_name
{
   ...
};

The only difference from the standard definition is that template parameters can occur in a block of code, in all syntactic constructs of the language, where it is permissible to use a type name.

Once a template is defined, working instances of it are created when the variables of the template type are declared in the code, specifying the specific types in angle brackets:

ClassName<Type1,Type2object;
StructName<Type1,Type2,Type3> struct;
ClassName<Type1,Type2> *pointer = new ClassName<Type1,Type2>();
ClassName1<ClassName2<Type>> object;

Unlike when calling template functions, the compiler is not able to infer actual types for object templates on its own.

Declaring a template class/structure variable is not the only way to instantiate a template. An instance is also generated by the compiler if a template type is used as the base type for another, specific (non-template) class or structure.

For example, the following class Worker, even if empty, is an implementation of Base for type double:

class Worker : Base<double>
{
};

This minimum definition is enough (with allowance for adding constructors if the class Base requires them) to start compiling and validating the template code.

In the Dynamic object creation section, we got acquainted with the concept of a dynamic pointer to an object obtained using the operator new. This flexible mechanism has one drawback: pointers need to be monitored and "manually" deleted when they are no longer needed. In particular, when exiting a function or block of code, all local pointers must be cleared with a call delete.

To simplify the solution to this problem, let's create a template class AutoPtr (TemplatesAutoPtr.mq5, AutoPtr.mqh). Its parameter T is used to describe the field ptr, which stores a pointer to an object of an arbitrary class. We will receive the pointer value through the constructor parameter (T *p) or in the overloaded operator '='. Let's entrust the main work to the destructor: in the destructor, the pointer will be deleted together with the object AutoPtr (the static helper method free is allocated for this).

The principle of operation of AutoPtr is simple: a local object of this class will be automatically destroyed upon exiting the block where it is described, and if it was previously instructed to "follow" some pointer, then AutoPtr will free it too.

template<typename T>
class AutoPtr
{
private:
   T *ptr;
   
public:
   AutoPtr() : ptr(NULL) { }
   
   AutoPtr(T *p) : ptr(p)
   {
      Print(__FUNCSIG__" ", &this": "ptr);
   }
   
   AutoPtr(AutoPtr &p)
   {
      Print(__FUNCSIG__" ", &this": "ptr" -> "p.ptr);
      free(ptr);
      ptr = p.ptr;
      p.ptr = NULL;
   }
   
   ~AutoPtr()
   {
      Print(__FUNCSIG__" ", &this": "ptr);
      free(ptr);
   }
   
   T *operator=(T *n)
   {
      Print(__FUNCSIG__" ", &this": "ptr" -> "n);
      free(ptr);
      ptr = n;
      return ptr;
   }
   
   Toperator[](int x = 0const
   {
      return ptr;
   }
   
   static void free(void *p)
   {
      if(CheckPointer(p) == POINTER_DYNAMICdelete p;
   }
};

Additionally, the class AutoPtr implements a copy constructor (more precisely, a jump constructor, since the current object becomes the owner of the pointer), which allows you to return an AutoPtr instance along with a controlled pointer from a function.

To test the performance of AutoPtr, we will describe a fictitious class Dummy.

class Dummy
{
   int x;
public:
   Dummy(int i) : x(i)
   {
      Print(__FUNCSIG__" ", &this);
   }
   ...
   int value() const
   {
      return x;
   }
};

In the script, in the OnStart function, enter the variable AutoPtr<Dummy> and get the value for it from the function generator. In the function generator itself, we will also describe the object AutoPtr<Dummy> and sequentially create and "attach" two dynamic objects Dummy to it (to check the correct release memory from the "old" object).

AutoPtr<Dummygenerator()
{
   AutoPtr<Dummyptr(new Dummy(1));
   // pointer to 1 will be freed after execution of '='
   ptr = new Dummy(2);
   return ptr;
}
   
void OnStart()
{
   AutoPtr<Dummyptr = generator();
   Print(ptr[].value());             // 2
}

Since all the main methods log object descriptors (both AutoPtr and controlled pointers ptr), we can track all "transformations" of pointers (for convenience, all lines are numbered).

01 Dummy::Dummy(int) 3145728
02  AutoPtr<Dummy>::AutoPtr<Dummy>(Dummy*) 2097152: 3145728
03  Dummy::Dummy(int) 4194304
04  Dummy*AutoPtr<Dummy>::operator=(Dummy*) 2097152: 3145728 -> 4194304
05  Dummy::~Dummy() 3145728
06  AutoPtr<Dummy>::AutoPtr<Dummy>(AutoPtr<Dummy>&) 5242880: 0 -> 4194304
07  AutoPtr<Dummy>::~AutoPtr<Dummy>() 2097152: 0
08  AutoPtr<Dummy>::AutoPtr<Dummy>(AutoPtr<Dummy>&) 1048576: 0 -> 4194304
09  AutoPtr<Dummy>::~AutoPtr<Dummy>() 5242880: 0
10  2
11  AutoPtr<Dummy>::~AutoPtr<Dummy>() 1048576: 4194304
12  Dummy::~Dummy() 4194304

Let's digress for a moment from the templates and describe in detail how the utility works because such a class can be useful to many.
 
Immediately after starting OnStart, the function generator is called. It must return a value to initialize the object AutoPtr in OnStart, and therefore its constructor has not yet been called. Line 02 creates an object AutoPtr#2097152 inside the function generator and gets a pointer to the first Dummy#3145728. Next, a second instance of Dummy#4194304 is created (line 03), which replaces the previous copy with descriptor 3145728 (line 04) in AutoPtr#2097152, and the old copy is deleted (line 05). Line 06 creates a temporary AutoPtr#5242880 to return the value from the generator, and deletes the local one (07). On line 08, the copy constructor for the AutoPtr#1048576 object in the function OnStart is finally used, and the pointer from the temporary object (which is immediately deleted on line 09) is transferred to it. Next, we call Print with the content of the pointer. When the OnStart completes, the destructor AutoPtr (11) automatically fires, causing us to also delete the work object Dummy (12).

Template technology makes the class AutoPtr a parameterized manager of dynamically allocated objects. But since AutoPtr has a field T *ptr, it only applies to classes (more precisely, pointers to class objects). For example, trying to instantiate a template for a string (AutoPtr<string> s) will result in a lot of errors in the template text, the meaning of which is that the string type does not support pointers.

This is not a problem here, since the purpose of this template is limited to classes, but for more general templates, this nuance should be kept in mind (see the sidebar).

Pointers and references
 
Please note that the T * construct cannot appear in templates that you plan to use, including for built-in types or structures. The point is that pointers in MQL5 are allowed only for classes. This is not to say that a template cannot in theory be written to apply to both built-in and user-defined types, but it may require some tweaking. It will probably be necessary either to abandon some of the functionality or to sacrifice a level of genericity of the template (make several templates instead of one, overload functions, etc.).
 
The most straightforward way to "inject" a pointer type into a template is to include the modifier '*' along with the actual type when the template is instantiated (i.e. it must match T=Type*). However, some functions (such as CheckPointer), operators (such as delete), and syntactic constructs (such as casting ((T)variable)) are sensitive to whether their arguments/operands are pointers or not. Because of this, the same template text is not always syntactically correct for both pointers and simple type values.
 
Another significant type difference to keep in mind: objects are passed to methods by reference only, but literals (constants) of simple types cannot be passed by reference. Because of this, the presence or absence of an ampersand may be treated as an error by the compiler, depending on the inferred type of T. As one of the "workarounds", you can optionally "wrap" argument constants into objects or variables.
 
Another trick involves using template methods. We will see it in the next section.

It should be noted that object-oriented techniques go well with patterns. Since a pointer to a base class can be used to store an object of a derived class, AutoPtr is applicable to objects of any derived Dummy classes.

In theory, this "hybrid" approach is widely used in the container classes (vector, queue, map, list, etc.), which, as a rule, are templates. Container classes may, depending on the implementation, impose additional requirements on the template parameter, in particular, that the inline type must have a copy constructor and an assignment (copy) operator.

The MQL5 standard library supplied with MetaTrader 5 contains many ready-made templates from this series: Stack.mqh, Queue.mqh, HashMap.mqh, LinkedList.mqh, RedBlackTree.mqh, and others. They are all located in the MQL5/Include/Generic directory. True, they do not provide control over dynamic objects (pointers).

We'll look at our own example of a simple container class in Method templates.