模板操作通用原则

我们来回想 函数重载。它包括用不同的参数定义一个函数的多个版本,包括参数数量相同但类型不同的情况。通常,对于不同类型的参数,此类函数的算法是相同的。例如,MQL5 有一个内置函数 MathMax ,可返回传递给它的两个值中较大的一个:

double MathMax(double value1double value2);

虽然只为 double 类型提供了原型,但该函数实际上能够处理其他数字类型的参数对,如 intdatetime。换句话说,该函数是内置数值类型的重载内核。如果我们想在源代码中实现同样的效果,就必须重载函数,方法是复制函数并搭配不同的参数,就像这样:

double Max(double value1double value2)
{
   return value1 > value2 ? value1 : value2;
}
 
int Max(int value1int value2)
{
   return value1 > value2 ? value1 : value2;
}
 
datetime Max(datetime value1datetime value2)
{
   return value1 > value2 ? value1 : value2;
}

所有实现(函数体)都是一样的。改变的只是参数类型。

这时,模板就派上用场了。通过使用模板,我们可以描述一个具有所需实现的算法样本,而编译器本身会根据程序中涉及的特定类型生成多个实例。模板的生成是在编译过程中即时进行的,程序员通常不会察觉到这个过程(除非模板代码本身有错误)。自动生成的源代码不会插入程序文本,而是直接转换成二进制代码(ex5 文件)。

在模板中,一个或多个参数是类型的形式化名称,在编译阶段,将根据特殊的类型推断规则,从内置类型或用户定义类型中选择实际类型。例如,Max 函数可以使用以下带有 T 类型参数的模板来描述:

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

然后将其应用于各种类型的变量(参见 TemplatesMax.mq5):

void OnStart()
{
   double d1 = 0d2 = 1;
   datetime t1 = D'2020.01.01', t2 = D'2021.10.10';
   Print(Max(d1d2));
   Print(Max(t1t2));
   ...
}

在这种情况下,编译器会自动为 doubledatetime 类型生成 Max 函数的变体。

模板本身不会生成源代码。为此,您需要以某种方式创建模板的实例:调用模板函数或提及具有特定类型的模板类名称,以创建对象或派生类。

在此之前,编译器将忽略整个模式。例如,我们可以编写下面这个所谓的模板函数,它实际上包含了语法错误的代码。但只要不在任何地方调用该函数,编译包含该函数的模块就会成功。

template<typename T>
void function()
{
  it's not a comment, but it's not source code either
   !%^&*
}

每次使用模板时,编译器都会确定与模板形式参数相匹配的实际类型。根据这些信息,将为每个独特的参数组合自动生成模板源代码。这就是实例。

因此,在我们给出的 Max 函数示例中,我们调用了两次模板函数:一次是针对一对 double 类型的变量,另一次是针对一对 datetime 类型的变量。这就产生了两个 Max 函数实例,其源代码分别用于匹配 T=doubleT=datetime。当然,如果在代码的其他部分调用了相同类型的模板,也不会生成新的实例。只有当模板应用于另一种类型(如果有不止 1 个参数,则为类型集)时,才需要一个新的模板实例。

请注意,Max 模板只有一个参数,它同时为函数的两个输入参数及其返回值设置了类型。换句话说,模板声明可以对有效自变量的类型施加某些限制。

如果我们对不同类型的变量调用 Max,编译器将无法确定实例化模板的类型,并会抛出错误“模棱两可的模板参数,必须是 'double' 或 'datetime'”:

Print(Max(d1t1)); // template parameter ambiguous,
                    // could be 'double' or 'datetime'

这种根据模板使用上下文来发现模板参数实际类型的过程称为类型推断。在 MQL5 中,类型推断仅适用于函数和方法模板。

而对于类、结构体和联合体,则使用另一种方式将类型绑定到模板参数:在创建模板实例时,在尖括号中显式指定所需的类型(如果有多个参数,则相应的类型数量将以逗号分隔列表形式指示)。有关更多信息,请参见 对象类型模板

同样的显式方法可以应用于函数,作为自动类型推断的替代方法。

例如,我们可以为 ulong 类型生成并调用 Max 实例:

Print(Max<ulong>(100010000000));

在这种情况下,如果没有明确指示,模板函数将与 int 类型相关联(基于整数常量的值)。