Funciones virtuales

La palabra clave virtual es un especificador de la función que proporciona el mecanismo para la elección dinámica durante la fase de ejecución de una función miembro conveniente entre las funciones de la clase base y derivada. Las estructuras no pueden tener funciones virtuales. Puede usarse para el cambio de las declaraciones sólo de las funciones miembro.

La función virtual, igual que una corriente, debe tener un cuerpo ejecutable. Durante la llamada su semántica es la misma que la de las demás funciones.

Una función virtual puede ser sustituida en una clase derivada. La elección de qué tipo de definición de la función llamar para una función virtual, se hace de una forma dinámica (durante la fase de ejecución). Un caso típico es cuando la clase base contiene una función virtual, y las clases derivadas tienen sus versiones de esta función.

El puntero a la clase base puede indicar al objeto de la clase base o al objeto de la clase derivada. La elección de la función miembro va a ser realizada en la fase de ejecución y va a depender del tipo del objeto y no del tipo del puntero. Si no hay miembro del tipo derivado, por defecto se utiliza la función virtual de la clase base.

Los destructores siempre son virtuales, independientemente de que si están declarados con la palabra clave virtual o no.

Vamos a ver el uso de funciones virtuales en el ejemplo del programa MT5_Tetris.mq5. La clase base CTetrisShape con la función virtual Draw (dibujar) está definida en el archivo incluido MT5_TetrisShape.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[]);
  };

Luego, para cada clase derivada, esta función se implementa de acuerdo con las particularidades de la clase descendiente. Por ejemplo, la primera figura CTetrisShape1 tiene su propia implementación de la función Draw():

class CTetrisShape1 : public CTetrisShape
  {
public:
   //--- dibujo de figura
   virtual void      Draw()
     {
      int    i;
      string name;
      //---
      if(m_turn==0 || m_turn==2)
        {
         //--- vara horizontal
         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
        {
         //--- vara vertical
         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);
           }
        }
     }
  }

La figura cuadrado está descrita por la clase CTetrisShape6 y tiene su propia implementación del método Draw():

class CTetrisShape6 : public CTetrisShape
  {
public:
   //--- dibujo de figura
   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);
        }
     }
  };

Dependiendo de qué clase ha sido creado el objeto, se llama a la función virtual de una u otra clase derivada.

void CTetrisField::NewShape()
  {
//--- creamos una de 7 posibles figuras de una manera aleatoria
   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;
     }
//--- dibujamos
   m_shape.Draw();
//---
  }

Modificador override #

El modificador override indica que la función declarada deberá redefinir obligatoriamente el método de la clase padre. El uso de este modificador permite evitar errores en la redifinición, tales como el cambio casual de la signatura del método. Por ejemplo, en la clase básica se define el método func, que toma como argumento una variable del tipo int:

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

A continuación, el método se redefine en la clase heredada:

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

Pero por error, el tipo de argumento cambia de int a short. De hecho, en este caso ya tiene lugar no la redefinición, sino la sobrecarga del método. Actuando de acuerdo con el algoritmo de definición de la función sobrecargada, en ciertas situaciones el compilador puede elegir el método definido en la clase básica, en lugar del método redefinido.

Para evitar errores semejantes, al método redefinido se le debe añadir claramente el modificador override.

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

Si durante la redefinición se cambia la signatura del método, el compilador no podrá encontrar en la clase padre un método con la misma signatura y dará un error de compilación:

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

Modificador final #

El modificador final actúa al revés, prohibiendo la redefinición del método en las clases heredadas. Si la implementación de un método es autosuficiente y ha finalizado por completo, declárela con el modificador final, para que haya garantías de que no será modificada en lo sucesivo.

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

Al intentar redefinir un método con el modificador final, como se muestra en el ejemplo de más arriba, el compilador dará error:

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

Véase también

Biblioteca estándar