仮想関数

virtual キーワードは、実行時に動的に基本クラスと派生クラスの関数から適切な関数メンバを選択するメカニズムを提供する関数指定子です。構造体は仮想関数を含むことが出来ません。virtual キーワードは、関数メンバのみの宣言を変更することが出来ます。

仮想関数は通常の関数同様に実行可能な本体を持たなければなりません。呼び出される際にはそのセマンティックは他の関数の物と同様です。

仮想関数は派生クラスでオーバーライドされる場合があります。仮想関数に対してどの関数定義が呼び出されるかは(実行時に)動的に選択されます。基本クラスに仮想関数が含まれ、派生クラスがこの関数の独自のバージョンを持っているのが典型的なケースです。

基本クラスへのポインタは、基本クラスオブジェクトまたは派生クラスのオブジェクトのいずれかを示すことが出来ます。呼び出されるメンバ関数の選択は実行時に行われ、ポインタの型ではなくオブジェクトの型に依存します。派生型のメンバが存在しない場合は、基本クラスの仮想関数がデフォルトで使用されます。

デストラクタvirtual キーワードで宣言されたかされなかったにかかわらずは常に仮想です。

MT5_Tetris.mq5 の例で仮想関数の使用を考えてみましょう。仮想関数 Draw を持つ基本クラス CTetrisShapeis が インクルードファイル MT5_TetisShape.mqh で定義されています。

//+------------------------------------------------------------------+
class CTetrisShape
 {
protected:
  int               m_type;
  int               m_xpos;
  int               m_ypos;
  int               m_xsize;
  int               m_ysize;
  int               m_prev_turn;
  int               m_turn;
  int               m_right_border;
public:
  void              CTetrisShape();
  void              SetRightBorder(int border) { m_right_border=border; }
  void              SetYPos(int ypos)          { m_ypos=ypos;           }
  void              SetXPos(int xpos)          { m_xpos=xpos;           }
  int               GetYPos()                  { return(m_ypos);        }
  int               GetXPos()                  { return(m_xpos);        }
  int               GetYSize()                 { return(m_ysize);       }
  int               GetXSize()                 { return(m_xsize);       }
  int               GetType()                  { return(m_type);        }
  void              Left()                     { m_xpos-=SHAPE_SIZE;    }
  void              Right()                    { m_xpos+=SHAPE_SIZE;    }
  void              Rotate()                   { m_prev_turn=m_turn; if(++m_turn>3) m_turn=0; }
  virtual void      Draw()                     { return;                }
  virtual bool      CheckDown(int& pad_array[]);
  virtual bool      CheckLeft(int& side_row[]);
  virtual bool      CheckRight(int& side_row[]);
 };

また、それぞれの派生クラスでは、この関数は子孫クラスの特性に応じて実施されます。例えば、最初の図形 CTetrisShape1 には独自の Draw() 関数が実装されています。

class CTetrisShape1 : public CTetrisShape
 {
public:
  //--- 図形を描画する
  virtual void      Draw()
    {
    int    i;
    string name;
    //---
    if(m_turn==0 || m_turn==2)
       {
        //--- 水平
        for(i=0; i<4; i++)
          {
           name=SHAPE_NAME+(string)i;
          ObjectSetInteger(0,name,OBJPROP_XDISTANCE,m_xpos+i*SHAPE_SIZE);
          ObjectSetInteger(0,name,OBJPROP_YDISTANCE,m_ypos);
          }
       }
    else
       {
        //--- 垂直
        for(i=0; i<4; i++)
          {
           name=SHAPE_NAME+(string)i;
          ObjectSetInteger(0,name,OBJPROP_XDISTANCE,m_xpos);
          ObjectSetInteger(0,name,OBJPROP_YDISTANCE,m_ypos+i*SHAPE_SIZE);
          }
       }
    }
 }

Square 図形は CTetrisShape6 クラスによって記述され独自の Draw() メソッドを実装します。

class CTetrisShape6 : public CTetrisShape
 {
public:
  //--- 図形を描画する
  virtual void      Draw()
    {
    int    i;
    string name;
    //---
    for(i=0; i<2; i++)
       {
        name=SHAPE_NAME+(string)i;
        ObjectSetInteger(0,name,OBJPROP_XDISTANCE,m_xpos+i*SHAPE_SIZE);
        ObjectSetInteger(0,name,OBJPROP_YDISTANCE,m_ypos);
       }
    for(i=2; i<4; i++)
       {
        name=SHAPE_NAME+(string)i;
        ObjectSetInteger(0,name,OBJPROP_XDISTANCE,m_xpos+(i-2)*SHAPE_SIZE);
        ObjectSetInteger(0,name,OBJPROP_YDISTANCE,m_ypos+SHAPE_SIZE);
       }
    }
 };

作成したオブジェクトが属するクラスに応じて、このクラスの仮想関数かその派生クラスの仮想関数が呼び出されます。

void CTetrisField::NewShape()
 {
//--- 7 つの可能な図形から 1 つをランダムに選んで作成する
  int nshape=rand()%7;
  switch(nshape)
    {
    case 0: m_shape=new CTetrisShape1; break;
    case 1: m_shape=new CTetrisShape2; break;
    case 2: m_shape=new CTetrisShape3; break;
    case 3: m_shape=new CTetrisShape4; break;
    case 4: m_shape=new CTetrisShape5; break;
    case 5: m_shape=new CTetrisShape6; break;
    case 6: m_shape=new CTetrisShape7; break;
    }
//--- 描画する
  m_shape.Draw();
//---
 }

override修飾子 #

override修飾子は、宣言される関数は必ず親クラスのメソッドをオーバーライドする必要があるということを意味しています。この修飾子を使用することで、オーバーライド時のメソッドシグネチャのランダムな変更といったエラーを回避することができます。例として、基底クラスで引数としてint型変数を取るfuncメソッドを定義します。

class CFoo
{
  void virtual func(int x) const { }
};

次にメソッドを派生クラスでオーバーライドします:

class CBar : public CFoo
{
  void func(short x) { }
};

しかしエラーによって引数の型がintからshortに変わります。実際には、このような場合、オーバーライドではなくメソッドのオーバーロードが起こります。オーバーロードされた関数定義のアルゴリズムに従い、特定のケースにおいてコンパイラがオーバーライドされたメソッドの代わりに規定クラスで定義されたメソッドを選択することがあります。

このようなエラーを回避する為には、明示的にオーバーライドメソッドにoverride修飾子を追加する必要があります。

class CBar : public CFoo
{
  void func(short x) override { }
};

オーバーライド時にメソッドシグネチャが変更された場合、コンパイラは親クラスで全く同じシグネチャを見つけることができず、コンパイルエラーを出します。

'CBar::func' method is declared with 'override' specifier but does not override any base class method

final修飾子 #

final修飾子は逆に、派生クラスでのメソッドのオーバーライドを禁止します。メソッドの実装が十分であり、完全に禁止されている場合、派生クラスで変更されないようにするためにfinal修飾子を使って宣言してください。

class CFoo
{
  void virtual func(int x) final { }
};
 
class CBar : public CFoo
{
  void func(int) { }
};
 

final修飾子を使用したメソッドのオーバーライドを試行すると、上記の例で示したように、コンパイルはエラーを出します。

'CFoo::func' method declared as 'final' cannot be overridden by 'CBar::func'
see declaration of 'CFoo::func'

参照

標準ライブラリ