运算符重载

表达式 一章中,我们学习了为内置类型定义的各种运算。例如,对于 double 类型的变量,我们可以计算以下表达式的值:

double a = 2.0b = 3.0c = 5.0;
double d = a * b + c;

使用类似的语法处理用户定义类型(例如矩阵)会很方便:

Matrix a(33), b(33), c(33); // creating 3x3 matrices
// ... somehow fill in a, b, c
Matrix d = a * b + c;

MQL5 通过运算符重载提供了这样的功能。

这种技术的组织方式是:通过描述一些方法来实现,其名称以 operator 关键字开头,后跟一个受支持的运算符号(或符号序列)。一般来说,这可以表示为如下形式:

result_type operator@ ( [type parameter_name] );

此处 @ - 运算符号。

完整的 MQL5 运算列表已在 运算优先级一节中提供,但并非所有运算都允许重载。

以下运算禁止重载:

  • 冒号 '::',上下文权限;
  • 括号 '()'、“函数调用”或“分组”;
  • 点 '.'、“取消引用”;
  • 与号 '&'、“取地址”、一元运算符(但与号可用作二元运算符“按位与”);
  • 条件三元运算符 '?:';
  • 逗号 ','。

所有其他运算符均可重载。重载运算符的优先级无法更改,它们保持与标准优先级相同,因此如有必要,应使用括号进行分组。

您不能为标准列表中未包含的某些新字符创建重载。

所有运算符的重载都会考虑其一元性和二元性,即保留所需操作数的数量。与任何类方法一样,运算符重载可以返回某种类型的值。在这种情况下,选择类型本身时应根据规划好的逻辑,也就是在表达式中使用函数所产生的结果(请参见下文)。

运算符重载方法的格式如下(用所需运算符的符号代替 '@' 符号):

名称

方法头

表达式中的
用法

函数
等同于

一元前缀

type operator@()

@object

object.operator@()

一元后缀

type operator@(int)

object@

object.operator@(0)

二元

type operator@(type parameter_name)

object@argument

object.operator@(argument)

索引

type operator[](type index_name)

object[argument]

object.operator[](argument)

一元运算符不接受参数。在一元运算符中,只有递增运算符 '++' 和递减运算符 '--' 除了支持前缀形式外,还支持后缀形式,所有其他一元运算符仅支持前缀形式。指定 int 类型的匿名参数即表示后缀形式(以区别于前缀形式),但参数本身会被忽略。

二元运算符必须接受一个参数。对于同一个运算符,可以存在多个重载的变体,这些变体可带有不同类型的参数,包括与当前对象所属的类相同的类型。在这种情况下,对象作为参数只能按引用或按指针进行传递(“按指针”仅适用于类对象,不适用于结构体)。

重载运算符既可以通过表达式中的运算语法使用(这是重载的主要原因),也可以通过方法调用语法使用;这两种方式都显示在上表中。这种功能等效性清晰地表明,从技术角度来说,运算符不过是对对象的方法调用,对于前缀运算符,对象在右侧,而对于所有其他运算符,对象在左侧。二元运算符方法将作为自变量传递给运算符右侧的值或表达式(特别要指出,这可以是另一个对象或内置类型的变量)。

由此可见,重载运算符不具有交换律属性:a@b 通常不等于 b@a,因为对于 a,@ 运算符可以重载,但 b 不能。此外,如果 b 是内置类型的变量或值,那么原则上不能重载它的标准行为。

举个例子,试想用于从斐波那契数列生成数字的 Fibo 类(我们已经使用函数完成了此任务的一个实现,请参见 函数定义)。在类中,我们将提供 2 个字段,分别用于存储行的当前编号和上一个编号:currentprevious。默认构造函数会将它们的值初始化为 1 和 0。我们还将提供一个拷贝构造函数 (FiboMonad.mq5)。

class Fibo
{
   int previous;
   int current;
public:
   Fibo() : current(1), previous(0) { }
   Fibo(const Fibo &other) : current(other.current), previous(other.previous) { }
   ...
};

对象的初始状态:当前编号为 1,上一个编号为 0。为了查找序列中的下一个编号,我们重载了前缀和后缀递增运算符。

   Fibo *operator++() // prefix
   {
      int temp = current;
      current = current + previous;
      previous = temp;
      return &this;
   }
   
   Fibo operator++(int// postfix
   {
      Fibo temp = this;
      ++this;
      return temp;
   }

请注意,前缀方法在编号修改后不会返回指向当前 Fibo 对象的指针,而后缀方法则会返回一个保存了上一个计数器的新对象,这符合后缀递增的原则。

当然,如果需要,程序员可以任意方式重载任何运算。例如,可以计算乘积、将数字输出到日志中,或者在递增的实现中执行其他操作。但还是建议遵循以下原则:使用运算符重载执行直观操作。

我们以类似的方式实现递减运算:递减运算将返回序列的上一个数字。

   Fibo *operator--() // prefix
   {
      int diff = current - previous;
      current = previous;
      previous = diff;
      return &this;
   }
   
   Fibo operator--(int// postfix
   {
      Fibo temp = this;
      --this;
      return temp;
   }

要获取给定数字在序列中的编号,我们需要重载索引访问运算。

   Fibo *operator[](int index)
   {
      current = 1;
      previous = 0;
      for(int i = 0i < index; ++i)
      {
         ++this;
      }
      return &this;
   }

要获取 current 变量中包含的当前数字,我们需要重载 '~' 运算符(因为它很少使用)。

   int operator~() const
   {
      return current;
   }

如果没有此重载,您仍然需要实现一些公共方法来读取私有字段 current。我们将使用此运算符通过 Print 输出数字。

为了方便起见,您也应该重载赋值运算。

   Fibo *operator=(const Fibo &other)
   {
      current = other.current;
      previous = other.previous;
      return &this;
   }
   
   Fibo *operator=(const Fibo *other)
   {
      current = other.current;
      previous = other.previous;
      return &this;
   }

我们来看看它是如何工作的。

void OnStart()
{
   Fibo f1f2f3f4;
   for(int i = 0i < 10; ++i, ++f1// prefix increment
   {
      f4 = f3++; // postfix increment and assignment overloading
   }
   
   // compare all values ​​obtained by increments and by index [10]
   Print(~f1" ", ~f2[10], " ", ~f3" ", ~f4); // 89 89 89 55
   
   // counting in opposite direction, down to 0
   Fibo f0;
   Fibo f = f0[10]; // copy constructor (due to initialization)
   for(int i = 0i < 10; ++i)
   {
      // prefix decrement
      Print(~--f); // 55, 34, 21, 13, 8, 5, 3, 2, 1, 1
   }
}

结果符合预期。不过,我们需要考虑一个细节。

   Fibo f5;
   Fibo *pf5 = &f5;
   
   f5 = f4;   // call Fibo *operator=(const Fibo &other) 
   f5 = &f4;  // call Fibo *operator=(const Fibo *other)
   pf5 = &f4// calls nothing, assigns &f4 to pf5!

指针赋值运算符的重载仅在通过对象访问时有效。如果通过指针访问,则采用指针间的标准赋值。

重载运算符的返回类型可以是内置类型、对象类型(类或结构体)或指针(仅限类对象)中的一种。

要返回对象(实例,而非引用),类必须实现拷贝构造函数。这种方式会导致实例重复,从而影响代码效率。如果可能,应该返回一个指针。

但在返回指针时,需要确保返回的不是局部自动对象(该对象在函数退出时会被删除,指针也将失效),而是某个已经存在的对象。通常会返回 &this

通过返回对象或对象的指针,您可以将一个重载运算符的结果“发送”给另一个重载运算符,从而以我们处理内置类型惯用的方式构建复杂的表达式。返回 void 将导致无法在表达式中使用该运算符。例如,如果 '=' 运算符定义为 void 类型,则多重赋值将失效:

Type xyz = 1// constructors and initialization of variables of a certain class
x = y = z// assignments, compilation error 

赋值链从右到左运行,y = z 将返回空。

如果对象仅包含内置类型的字段(包括数组),则无需重新定义来自同一类中对象的赋值/拷贝运算符 '=':MQL5 默认提供所有字段的“一对一”拷贝。请勿将赋值/拷贝运算符与拷贝构造函数和初始化混淆。

现在,我们来看第二个示例:使用矩阵 (Matrix.mq5)。

顺便提一下,内置对象类型 矩阵和向量 最近已出现在 MQL5 中。使用内置类型还是自定义类型(或者将它们组合使用)由每位开发者自行决定。内置类型中包含许多常用方法的现成快速实现,非常方便,避免了繁琐的编程工作。而自定义类允许您根据任务调整算法。我们在这里提供了 Matrix 类作为教程。

在矩阵类中,我们将其元素存储在一维动态数组 m 中。在“sizes”下,选择变量 rowscolumns

class Matrix
{
protected:
   double m[];
   int rows;
   int columns;
   void assign(const int rconst int cconst double v)
   {
      m[r * columns + c] = v;
   }
      
public:
   Matrix(const Matrix &other) : rows(other.rows), columns(other.columns)
   {
      ArrayCopy(mother.m);
   }
   
   Matrix(const int rconst int c) : rows(r), columns(c)
   {
      ArrayResize(mrows * columns);
      ArrayInitialize(m0);
   }

主构造函数接受两个参数(矩阵维度)并为数组分配内存。还有一个拷贝构造函数来自另一个矩阵 other。此处和下文中大量使用了用于处理数组的内置函数(具体而言是 ArrayCopyArrayResizeArrayInitialize),我们将在单独 章节中讨论这些函数。

我们通过重载赋值运算符来安排从外部数组填充元素的操作:

   Matrix *operator=(const double &a[])
   {
      if(ArraySize(a) == ArraySize(m))
      {
         ArrayCopy(ma);
      }
      return &this;
   }

为了实现两个矩阵的加法,我们重载了 '+=' 和 '+' 运算符:

   Matrix *operator+=(const Matrix &other)
   {
      for(int i = 0i < rows * columns; ++i)
      {
         m[i] += other.m[i];
      }
      return &this;
   }
   
   Matrix operator+(const Matrix &other) const
   {
      Matrix temp(this);
      return temp += other;
   }

请注意,'+=' 运算符在修改当前对象后返回该对象的指针,而 '+' 运算符则按值返回一个新实例(将使用拷贝构造函数),并且该运算符本身带有 const 修饰符,因此不会更改当前对象。

'+' 运算符本质上是一个包装器,它先创建当前矩阵的临时副本以备调用(命名为 temp),然后将所有工作委派给 ‘+=’ 运算符。因此,通过内部调用 '+=' 运算符(会修改 temp)将 tempother 相加,然后返回 ' +' 运算的结果。

矩阵乘法的重载方式类似,使用两个运算符 '*=' 和 '*'。

   Matrix *operator*=(const Matrix &other)
   {
      // multiplication condition: this.columns == other.rows
     // the result will be a matrix of size this.rows by other.columns
      Matrix temp(rows, other.columns);
      
      for(int r = 0r < temp.rows; ++r)
      {
         for(int c = 0c < temp.columns; ++c)
         {
            double t = 0;
            //we add up the pairwise products of the i-th elements
           // row 'r' of the current matrix and column 'c' of the matrix other
            for(int i = 0i < columns; ++i)
            {
               t += m[r * columns + i] * other.m[i * other.columns + c];
            }
            temp.assign(rct);
         }
      }
      // copy the result to the current object of the matrix this
      this = temp// calling an overloaded assignment operator
      return &this;
   }
   
   Matrix operator*(const Matrix &other) const
   {
      Matrix temp(this);
      return temp *= other;
   }

下面,我们将矩阵乘以一个数字:

   Matrix *operator*=(const double v)
   {
      for(int i = 0i < ArraySize(m); ++i)
      {
         m[i] *= v;
      }
      return &this;
   }
   
   Matrix operator*(const double vconst
   {
      Matrix temp(this);
      return temp *= v;
   }

为了比较两个矩阵,我们使用运算符 '==' 和 '!=':

   bool operator==(const Matrix &otherconst
   {
      return ArrayCompare(mother.m) == 0;
   }
   
   bool operator!=(const Matrix &otherconst
   {
      return !(this == other);
   }

为了方便调试,我们将矩阵数组的输出写入日志。

   void print() const
   {
      ArrayPrint(m);
   }

除了上述重载之外,Matrix 类还提供了运算符 [] 的重载:此运算符可返回嵌套类 MatrixRow 的对象,即具有给定编号的行。

   MatrixRow operator[](int r)
   {
      return MatrixRow(thisr);
   }

MatrixRow 类本身通过重载相同的运算符 [] 提供了对矩阵元素的更深层访问(也就是说,对于矩阵,可以自然地指定两个索引 m[i][j])。

   class MatrixRow
   {
   protected:
      const Matrix *owner;
      const int row;
      
   public:
      class MatrixElement
      {
      protected:
         const MatrixRow *row;
         const int column;
         
      public:
         MatrixElement(const MatrixRow &mrconst int c) : row(&mr), column(c) { }
         MatrixElement(const MatrixElement &other) : row(other.row), column(other.column) { }
         
         double operator~() const
         {
            return row.owner.m[row.row * row.owner.columns + column];
         }
         
         double operator=(const double v)
         {
            row.owner.m[row.row * row.owner.columns + column] = v;
            return v;
         }
      };
   
      MatrixRow(const Matrix &mconst int r) : owner(&m), row(r) { }
      MatrixRow(const MatrixRow &other) : owner(other.owner), row(other.row) { }
      
      MatrixElement operator[](int c)
      {
         return MatrixElement(thisc);
      }
   
      double operator[](uint c)
      {
         return owner.m[row * owner.columns + c];
      }
   };

int 类型的参数,[] 运算符会返回 MatrixElement 类的对象,您可以通过该对象在数组中写入特定元素。要读取元素,需要将 [] 运算符与 uint 类型参数一起使用。这看起来像是一个技巧,但实际上是语言方面的局限性:重载的参数类型必须不同。作为读取元素的替代方法,MatrixElement 类提供了运算符 '~' 的重载。

使用矩阵时,通常需要一个单位矩阵,因此我们来为其创建一个派生类:

class MatrixIdentity : public Matrix
{
public:
   MatrixIdentity(const int n) : Matrix(nn)
   {
      for(int i = 0i < n; ++i)
      {
         m[i * rows + i] = 1;
      }
   }
};

现在,我们开始实际使用矩阵表达式。

void OnStart()
{
   Matrix m(23), n(32); // description
   MatrixIdentity p(2);     // identity matrix
   
   double ma[] = {-1,  0, -3,
                   4, -5,  6};
   double na[] = {7,  8,
                  9,  1,
                  2,  3};
   m = ma// filling in data
   n = na;
   
   //we can read and write elements separately
   m[0][0] = m[0][(uint)0] + 2// variant 1 
   m[0][1] = ~m[0][1] + 2;      // variant 2 
   
   Matrix r = m * n + p;                    // expression
   Matrix r2 = m.operator*(n).operator+(p); // equivalent
   Print(r == r2); // true
   
   m.print(); // 1.00000  2.00000 -3.00000  4.00000 -5.00000  6.00000
   n.print(); // 7.00000 8.00000 9.00000 1.00000 2.00000 3.00000
   r.print(); // 20.00000  1.00000 -5.00000  46.00000
}

这里,我们分别创建了两个矩阵:3*2 维和 2*3 维,然后用数组中的值填充这两个矩阵,并使用两个索引 [][] 的语法编辑选定元素。最后,我们计算了表达式 m * n + p,其中所有操作数都是矩阵。下面一行以方法调用的形式显示了相同的表达式。我们得到了相同的结果。

与 C++ 不同,MQL5 不支持全局级别的运算符重载。在 MQL5 中,运算符只能在类或结构体的上下文中重载,也就是说,通过类或结构体的方法进行重载。另外,MQL5 不支持重载类型转换运算符 newdelete