Features of built-in and object types in templates

It should be kept in mind that 3 important aspects impose restrictions on the applicability of types in a template:

  • Whether the type is built-in or user-defined (user-defined types require parameters to be passed by reference, and built-in ones will not allow a literal to be passed by reference);
  • Whether the object type is a class (only classes support pointers);
  • A set of operations performed on data of the appropriate types in the template algorithm.

Let's say we have a Dummy structure (see script TemplatesMax.mq5):

struct Dummy
{
   int x;
};

If we try to call the Max function for two instances of the structure, we will get a bunch of error messages, with mains as the following: "objects can only be passed by reference" and "you cannot apply a template."

   // ERRORS:
   // 'object1' - objects are passed by reference only
   // 'Max' - cannot apply template
   Dummy object1object2;
   Max(object1object2);

The pinnacle of the problem is passing template function parameters by value, and this method is incompatible with any object type. To solve it, you can change the type of parameters to links:

template<typename T>
T Max(T &value1T &value2)
{
   return value1 > value2 ? value1 : value2;
}

The old error will go away, but then we will get a new error: "'>' - illegal operation use" ("'>' - illegal operation use"). The point is that the Max template has an expression with the '>' comparison operator. Therefore, if a custom type is substituted into the template, the '>' operator must be overloaded in the template (and the structure Dummy does not have it: we'll get to that shortly). For more complex functions, you will likely need to overload a much larger number of operators. Fortunately, the compiler tells you exactly what is missing.

However, changing the method of passing function parameters by reference additionally led to the previous call not working as such:

Print(Max<ulong>(100010000000));

Now it generates errors: "parameter passed as reference, variable expected". Thus, our function template stopped working with literals and other temporary values ​​(in particular, it is impossible to directly pass an expression or the result of calling another function into it).

One might think that the universal way out of the situation would be template function overloading, i.e., the definition of both options, that differs only in the ampersand in the parameters:

template<typename T>
T Max(T &value1T &value2)
{
   return value1 > value2 ? value1 : value2;
}
  
template<typename T>
T Max(T value1T value2)
{
   return value1 > value2 ? value1 : value2;
}

But it won't work. Now the compiler throws the error "ambiguous function overload with the same parameters":

'Max' - ambiguous call to overloaded function with the same parameters
could be one of 2 function(s)
   T Max(T&,T&)
   T Max(T,T)

The final, working overload would require the modifier const to be added to the links. Along the way, we added the operator Print to the template Max so that we can see in the log which overload is being called and which parameter type T corresponds to.

template<typename T>
T Max(const T &value1const T &value2)
{
   Print(__FUNCSIG__" T="typename(T));
   return value1 > value2 ? value1 : value2;
}
   
template<typename T>
T Max(T value1T value2)
{
   Print(__FUNCSIG__" T="typename(T));
   return value1 > value2 ? value1 : value2;
}
   
struct Dummy
{
   int x;
   bool operator>(const Dummy &otherconst
   {
      return x > other.x;
   }
};

We have also implemented an overload of the operator '>' in the Dummy structure. Therefore, all Max function calls in the test script are completed successfully: both for built-in and user-defined types, as well as for literals and variables. The outputs that go into the log:

double Max<double>(double,double) T=double
1.0
datetime Max<datetime>(datetime,datetime) T=datetime
2021.10.10 00:00:00
ulong OnStart::Max<ulong>(ulong,ulong) T=ulong
10000000
Dummy Max<Dummy>(const Dummy&,const Dummy&) T=Dummy

An attentive reader will notice that we now have two identical functions that differ only in the way parameters are passed (by value and by reference), and this is exactly the situation against which the use of templates is directed. Such duplication can be costly if the function body is not as simple as ours. This can be solved by the usual methods: separate the implementation into a separate function and call it from both "overloads", or call one "overload" from the other (an optional parameter was required to avoid the first version of Max calling itself and, resulting in stack overflows):

template<typename T>
T Max(T value1T value2)
{
   // calling a function with parameters by reference
   return Max(value1value2true);
}
   
template<typename T>
T Max(const T &value1const T &value2const bool ref = false)
{
   return (T)(value1 > value2 ? value1 : value2);
}

We still have to consider one more point associated with user-defined types, namely the use of pointers in templates (recall, that they apply only to class objects). Let's create a simple class Data and try to call the template function Max for pointers to its objects.

class Data
{
public:
   int x;
   bool operator>(const Data &otherconst
   {
      return x > other.x;
   }
};
   
void OnStart()
{
   ... 
   Data *pointer1 = new Data();
   Data *pointer2 = new Data();
   Max(pointer1pointer2);
   delete pointer1;
   delete pointer2;
}

We will see in the log that 'T=Data*', i.e. the pointer attribute, hits the inline type. This suggests that, if necessary, you can write another overload of the template function, which will be responsible only for pointers.

template<typename T>
T *Max(T *value1T *value2)
{
   Print(__FUNCSIG__" T="typename(T));
   return value1 > value2 ? value1 : value2;
}

In this case, the attribute of the pointer '*' is already present in the template parameters, and so type inference results in 'T=Data'. This approach allows you to provide a separate template implementation for pointers.

If there are multiple templates that are suitable for generating an instance with specific types, the most specialized version of the template is chosen. In particular, when calling the function Max with pointer arguments, two templates with parameters T (T=Data*) and T* (T=Data), but since the former can take both values ​​and pointers, it is more general than the latter, which only works with pointers. Therefore, the second one will be chosen for pointers. In other words, the fewer modifiers in the actual type that is substituted for T, the more preferable the template variant. In addition to the attribute of the pointer '*', this also includes the modifier const. The parameters const T* or const T are more specialized than just T* or T, respectively.