方法模板

不仅整个对象类型可以作为模板,其单独的方法(简单方法或静态方法)也可以作为模板。虚方法是个例外:它们不能被声明为模板。因此,模板方法不能在 接口一节中显示了相关示例。但是,接口本身可以作为模板,而虚方法可以存在于类模板中。

当方法模板包含在类/结构体模板中时,两个模板的参数必须不同。如果有多个模板方法,它们的参数之间没有任何关联,并且可以具有相同的名称。

方法模板的声明方式类似于 函数模板,但仅限于类、结构体或联合体(它们可能是也可能不是模板)的上下文中。

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

参数、返回值和方法主体可以使用类型 T(类的通用类型)和 U(方法的特定类型)。

只有在程序代码中调用特定参数组合的方法时,才会生成该方法的实例。

在上一节中,我们描述了用于存储和释放单个指针的模板类 AutoPtr。当存在多个相同类型的指针时,将它们放入容器对象中会很方便。我们创建一个具有类似功能的简单模板―SimpleArray 类 (SimpleArray.mqh)。为了避免重复控制动态内存释放的功能,我们将在类契约中声明它用于存储值和对象,而不是指针。为了存储指针,我们将它们放入 AutoPtr 对象中,然后将这些对象放入容器中。

这还有另一个好处:由于 AutoPtr 对象很小,因此易于复制(不会耗费过多资源),这在函数之间交换数据时经常发生。AutoPtr 指向的应用程序类的对象可以很大,甚至不需要在其中实现自己的拷贝构造函数。

当然,从函数返回指针的开销更小,但您需要重新设计内存释放控制的方法。因此,使用 AutoPtr 形式的现成解决方案更为简单。

对于容器内的对象,我们将创建模板类型 T 的 data 数组。

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

由于容器的主要操作之一是添加元素,因此我们将提供一个辅助函数来扩展数组。

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

我们将直接通过重载运算符 '<<' 添加元素。它使用通用模板参数 T。

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

此选项通过引用获取值,即变量或对象。您现在应该注意这一点,这一点的重要性稍后就会显现。

读取元素是通过重载运算符 '[]' 来完成的(它具有最高优先级,因此表达式中不需要使用括号)。

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

首先,我们确保该类能够处理结构体示例。

struct Properties
{
   int x;
   string s;
};

为此,我们将在 OnStart 函数中描述一个用于该结构体的容器,并将一个对象 (TemplatesSimpleArray.mq5) 放入其中。

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

通过调试日志记录,您可以验证该结构体是否位于容器中。

现在,让我们尝试在容器中存储一些数字。

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

遗憾的是,我们会收到“参数作为引用传递,应为变量”的错误,而这恰恰发生在重载运算符 '<<' 中。

我们需要一个通过值传递参数的重载。但是,我们不能直接写一个不包含 const 和 '&' 的类似方法:

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

如果这样做,新的变体将导致对象类型的模板无法编译:毕竟,对象只需要通过引用传递。即使该函数不用于对象,它仍然存在于类中。因此,我们将新方法定义为带有自身参数的模板。

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

只有当通过值传递数据给 '<<' 运算符时,它才会出现在类中,这表明传递的绝对不是一个对象。诚然,我们无法保证 T 和 U 相同,因此会执行显式强制类型转换 (T)u。对于内置类型(如果两种类型不匹配),在某些情况下,转换可能会损失精度,但代码肯定能通过编译。唯一的例外是禁止将字符串转换为布尔类型,但该容器不太可能用于 bool 数组,因此这个限制并不重要。希望解决这个问题的人可以自行解决。

使用新的模板方法,容器 SimpleArray<double> 可以正常工作,并且不会与 SimpleArray<Properties> 冲突,因为这两个模板实例在生成的源代码中存在差异。

最后,我们检查一下包含 AutoPtr 对象的容器。为此,我们准备一个简单的 Dummy 类,它将为 AutoPtr 内部的指针“提供”对象。

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

OnStart 函数中,创建一个容器 SimpleArray<AutoPtr<Dummy>> 并填充它。

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());
}

回想一下,在 AutoPtr 中,运算符 '[]' 用于返回一个存储的指针,因此 arrayObjects[0][] 的含义是:将 data 数组的第 0 个元素返回到 SimpleArray 中,即 AutoPtr 对象,然后将第二对方括号应用于该卷,得到一个指针 Dummy*。接下来,我们可以操作该对象的所有特性:在本例中,我们获取 x 字段的当前值。

由于 Dummy 没有拷贝构造函数,因此如果不使用 AutoPtr 便无法使用容器直接存储这些对象。

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

但机智的用户应该能猜到如何解决这个问题。

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

这段代码可以编译并运行。然而,这个“解决方案”存在一个问题:SimpleArray 不知道如何控制指针,因此,当程序退出时,会检测到内存泄漏。

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

作为 SimpleArray 的开发者,我们有责任弥补这个漏洞。为此,我们向类中添加另一个模板方法,并重载运算符 '<<' ―这次是针对指针。由于它是一个模板,因此它只会在“按需”包含在生成的源代码中:当程序员尝试使用此重载时,即向容器写入一个指针时。否则,该方法将被忽略。

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

当用指针类型实例化一个模板时,这种特化会抛出编译错误(“需要对象指针”)。因此,我们会告知用户不支持此模式。

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

此外,它还会执行另一项保护措施。如果客户端类仍然具有拷贝构造函数,那么将动态分配的对象保存在容器中将不再导致内存泄漏:所传递指针 P *p 处的对象副本将保留在容器中,而原始对象将被删除。当容器在 OnStart 函数末尾被销毁时,其内部 data 数组将自动调用其元素的析构函数。

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

方法模板和“简单”方法可以在主类块(或类模板)之外定义,类似于我们在 拆分类的声明和定义 一节中看到的内容。同时,它们前面都带有模板头文件 (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;

它还展示了模板化静态变量的初始化,表明采用了单例模式。

OnStart 函数中,创建模板实例并进行测试:

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)
}