函数模板

函数模板由一个包含模板参数的头文件(其语法已在 上文介绍过)和一个函数定义组成,在函数定义中,模板参数表示任意类型。

作为第一个例子,假设有一个 Swap 函数用于交换两个数组元素 (TemplatesSorting.mq5)。模板参数 T 用作输入数组变量的类型以及局部变量 temp 的类型。

template<typename T>
void Swap(T &array[], const int iconst int j)
{
   const T temp = array[i];
   array[i] = array[j];
   array[j] = temp;
}

函数主体中的所有语句和表达式都必须适用于实数类型,模板随后将针对这些实数类型进行实例化。在本例中,使用了赋值运算符 '='。虽然它对于内置类型始终存在,但对于用户定义类型,可能需要显式重载。

编译器默认为类和结构体生成拷贝运算符的实现,但用户可以隐式或显式地删除(参见 delete关键字)。具体来说,正如我们在 对象类型转换一节中看到的,类中存在常量字段会导致编译器移除其隐式拷贝选项。因此,上述 Swap 模板函数不能用于此类的对象:编译器会报错。

对于 Swap 函数使用的类/结构体,最好不仅包含赋值运算符,还包含拷贝构造函数,因为 temp 变量的声明实际上是一个带有初始化的构造,而不是赋值。使用拷贝构造函数时,函数的第一行会一次性执行(temp 是基于 array[i] 创建的);如果没有拷贝构造函数,则会先调用默认构造函数,然后对 temp 执行运算符 '='。

我们来看看如何在快速排序算法中使用 Swap 模板函数:另一个 QuickSort 模板函数实现了该算法。

template<typename T>
void QuickSort(T &array[], const int start = 0int end = INT_MAX)
{
   if(end == INT_MAX)
   {
      end = start + ArraySize(array) - 1;
   }
   if(start < end)
   {
      int pivot = start;
      
      for(int i = starti <= endi++)
      {
         if(!(array[i] > array[end]))
         {
            Swap(arrayipivot++);
         }
      }
      
      --pivot;
   
      QuickSort(arraystartpivot - 1);
      QuickSort(arraypivot + 1end);
   }
}

请注意,QuickSort 模板的 T 参数指定了输入参数 array 的类型,然后该数组会被传递给 Swap 模板。因此,QuickSort 模板的类型推断 T 将自动确定 Swap 模板的类型 T。

内置函数 ArraySize(与许多其他函数一样)能够处理任意类型的数组:从某种意义上说,它也是一个模板,尽管它是直接在终端中实现的。

排序是通过 if 语句中的 '>' 比较运算符完成的。如前所述,必须为任何要排序的 T 类型定义此运算符,因为它适用于 T 类型数组的元素。

我们来了解一下内置类型数组的排序是如何进行的。

void OnStart()
{
   double numbers[] = {3411, -74915, -10011};
   QuickSort(numbers);
   ArrayPrint(numbers);
   // -100.00000 -7.00000 11.00000 11.00000 15.00000 34.00000 49.00000
   
   string messages[] = {"usd""eur""jpy""gbp""chf""cad""aud""nzd"};
   QuickSort(messages);
   ArrayPrint(messages);
   // "aud" "cad" "chf" "eur" "gbp" "jpy" "nzd" "usd"
}

两次调用 QuickSort 模板函数会根据传入数组的类型自动推断 T 的类型。因此,我们将获得两个分别对应 doublestring 类型的 QuickSort 实例。

为了检查自定义类型的排序,我们创建一个包含 x 整数字段的 ABC 结构体,并在构造函数中用随机数填充此结构体。在结构体中重载 '>' 运算符也很重要。

struct ABC
{
   int x;
   ABC()
   {
      x = rand();
   }
   bool operator>(const ABC &otherconst
   {
      return x > other.x;
   }
};
void OnStart()
{
   ...
   ABC abc[10];
   QuickSort(abc);
   ArrayPrint(abc);
  /* Sample output:
            [x]
      [0]  1210
      [1]  2458
      [210816
      [313148
      [415393
      [520788
      [624225
      [729919
      [832309
      [932589
   */
}

由于结构体值是随机生成的,我们会得到不同的结果,但它们始终按升序排列。

在这种情况下,也会自动推断出 T 类型。但在某些情况下,显式指定是将类型传递给函数模板的唯一方法。因此,如果模板函数必须返回唯一类型的值(与其参数的类型不同),如果没有参数,则只能显式指定。

例如,以下 createInstance 模板函数需要在调用指令中显式指定类型,因为无法根据返回值自动“计算”类型 T。如果不这样做,编译器会生成“模板不匹配”错误。

class Base
{
   ...
};
   
template<typename T>
T *createInstance()
{
   T *object = new T(); //calling the constructor
   ...                  //object setting
   return object
}
   
void OnStart()
{
   Base *p1 = createInstance();       // error: template mismatch
   Base *p2 = createInstance<Base>(); // ok, explicit directive
   ...
}

如果有多个模板参数,并且返回值的类型未绑定到函数的任何输入参数,则在调用时还需要指定特定类型:

template<typename T,typename U>
T MyCast(const U u)
{
   return (T)u;
}
   
void OnStart()
{
   double d = MyCast<double,string>("123.0");
   string f = MyCast<string,double>(123.0);
}

请注意,如果显式指定了模板的类型,则所有参数都需要这样做,即使第二个参数 U 可以从传递的自变量中推断出来。

编译器生成模板函数的所有实例后,将参与标准流程,从所有具有相同名称且参数数量合适的 函数重载 中选择最佳候选项。在所有重载选项(包括已创建的模板实例)中,选择类型最接近(转换次数最少)的选项。

如果模板函数包含某些特定类型的输入参数,则只有当这些类型与自变量完全匹配时,它才被视为候选:任何转换需求都会导致模板因不合适而被“丢弃”。

非模板重载优先于模板重载,越特化(“范围越狭窄”),就越容易在模板重载中胜出。

如果显式指定了模板自变量(类型),并且这些类型不同,则将对相应的函数自变量(传递的值)应用 隐式类型转换 规则(如有必要)。

如果一个函数的多个变体匹配度相同,我们将收到“对具有相同参数的重载函数的调用有歧义”错误。

例如,如果除了 MyCast 模板之外,还定义了一个将字符串转换为布尔类型的函数:

bool MyCast(const string u)
{
   return u == "true";
}

则调用 MyCast<double,string>("123.0") 将开始抛出指示的错误,因为这两个函数仅在返回值上有所不同:

'MyCast<double,string>' - ambiguous call to overloaded function with the same parameters
could be one of 2 function(s)
   double MyCast<double,string>(const string)
   bool MyCast(const string)

描述模板函数时,建议将所有模板参数包含在函数参数中。类型只能从自变量推断,而不能从返回值推断。

如果函数有一个带有默认值的模板化类型参数 T,并且在调用时省略了相应的参数,那么编译器也将无法推断 T 的类型,并抛出“无法应用模板”错误。

class Base
{
public:
   Base(const Base *source = NULL) { }
   static Base *type;
};
   
static BaseBase::type;
   
template<typename T>
T *createInstanceFrom(T *origin = NULL)
{
   T *object = new T(origin);
   return object
}
   
void OnStart()
{
   Base *p1 = createInstanceFrom();   // error: cannot to apply template
   Base *p2 = createInstanceFrom(Base::type); // ok, auto-detect from argument
   Base *p3 = createInstanceFrom<Base>();     // ok, explicit directive, an argument is omitted
}