bclose

Unary Operator Overloading

¿Sobrecarga de operadores unitarios?

Objetivos

 

 

    • Operadores unitarios y binarios.
    • Sobrecarga de operadores unitarios
    • Operadores unitarios en prefijo y sufijo.
    • Devolviendo objetos como retorno de una función.
    • El operador this   
 
 

Material requerido.

Imagen de Arduino UNOArduino Uno o similar.

 

Paz InteriorMucha paz interior

Operator Overloading

 

En la última sesión estabamos construyendo una Clase, Contador, que nos sirviera como ejemplo de lo que podemos hacer. Vimos cómo definir la sintaxis y sobre todo nos centramos en el Function Overloading, ya que nos daba una ventaja importante de cara a usar un nombre único de función, para varis cosas que en principio serian diferentes.

La ventaja de esto es que resulta  mucho más fácil de recordar y más sencillo de utilizar porque encaja bien con nuestra forma de procesar las ideas.

Pero una vez que abrimos la caja de Pandora con el Overloading, resulta muy complicado cerrarla, porque en cuanto te acostumbras a la idea, empiezas a hacerte muchas preguntas raras, del tipo de ¿Y qué más puedo sobrecargar? Y aquí es cuando la cosa se lía.

Porque no solo se pueden sobrecargar las funciones, sino también los operadores para que hagan cosas diferentes en función del tipo de los operadores. No creo que tenga que insistir mucho para que me creáis si os digo que la suma de dos enteros no se parece (A nivel de procedimiento) a la de dos float,  y lo mismo pasa con +, -, * y / por poner un caso.

Los operadores invocan distintos procedimientos en función del tipo de los operandos, y nunca es más evidente que cuando hacemos:

String Duplica( String s)
  {    return ( s + s) ;  }

En donde el símbolo de la suma significa concatenar dos Strings. Todos estos operadores están sobrecargados por C++, para que podamos usarlos sin pensar en ello, y que se comporten como parece que es lo normal.  (Pero no podemos por ejemplo hacer s1-s2, porque ¿Qué sentido tendría?)

De hecho, cuando definimos una nueva Clase, lo que estamos haciendo es crear un nuevo tipo de datos, tipo en el sentido de int, long, etc. y dentro de cada clase podemos hacer el Overloading de los operadores que nos interesen, para indicarle al compilador, como debe ejecutarse la operación que representa el símbolo del operador.

Por eso vamos a dedicar esta sesión a ver la forma y el modo de realizar el Operator Overloading, pero os prevengo, sentaros cómodos y a ser posible relajados, porque el tema hay que irlo dosificando sin prisa.

Pero antes me gustaría hablaros de los operadores unarios y binarios. En C++, se consideran dos grandes familias de operadores, los que se aplican a un solo elemento (Unary Operator u Operador unitario) y los que se aplican a 2 elementos (O Binary Operator, Operador Binario).

 
  • También existe un operador terciario que no nos conviene mencionar en este momento.   

En la primera categoría, Unary Operators,  están los operadores de incrementar y decrementar ++ y –, tanto en su versión prefijo como sufijo (++i, i++) y además la negación y el símbolo negativo – cuando se aplica a un número para cambiarle el signo. En la categoría de Binary Operators tenemos +, -, *, /, % entre otros.

Esto es importante porque vamos a empezar viendo como se hace el Operator Overload de los Unary Operators (No corráis cobardes)

 

Unary Operator Overload

 

Volvamos a nuestra flamante nueva Clase de Contador, para usarla como base. Podemos reescribirla  así:

class Contador
   { private:
         int N ;
     
     public:
         Contador( ) : N(0) {}                      // Constructor
         Contador(int k ) : N(k) {}                 // Constructor
         void SetContador( int n) ;
         void Incrementar() ;
         int GetCont() ;
   } ;

void Contador::SetContador( int n)
     {  N = n ;    }
void Contador::Incrementar()
     {  N++ ; }
int Contador::GetCont()
     { return (N) ;}

Hemos reescrito los constructores para tener una notación más compacta. Bien no está mal. Podemos inicializar  los objetos de Contador, con y sin parámetro, lo que es un avance y nos permite escribir tal y como veíamos en la última sesión algo así:

Contador C1, C2(23) ;

Lo que resulta bastante fácil de leer, y cómodo de usar, pero ya que estamos (Ay Dios) nos preguntamos si se podrían  hacer algunas cosas normales en C++ como esto:

++C2 ;

En lugar de nuestra forma actual:

C2.Incrementar() ;

Que es como un poco raro de leer. ¿Sería posible? Intentadlo y veréis lo que dice el compilador.

Mensaje de error

Recordad que dijimos que crear una Clase es como crear un nuevo tipo de datos. El compilador sabe cómo aplicar el operador ++ a un int, pero no tiene ni idea de cómo usarlo con un Contador… salvo que se lo expliquemos claramente, con un Operator Overload.

La cosa está chupada. Para ello basta con redefinir el operador ++ para nuestra clase mediante la instrucción operator y nuestra clase quedaría:

class Contador
   {  private:
         int N ;

      public:
         Contador( ) : N(0) {}                    // Constructor
         Contador(int k ) : N(k) {}               // Constructor
         void SetContador( int n) ;
         int GetCont() ;
         void operator ++ ();                         // Aqui esta ++
   } ;

void Contador::SetContador( int n)  {  N = n ;    }
int  Contador::GetCont() { return (N) ;}
void Contador::operator ++ ()                         //  <---
     {  ++N }

En la que podéis ver que la línea clave es :

void operator ++ ();

Usamos la keyword “operator”, para identificar el operador a definir y la definimos como void porque no devolvemos nada, simplemente incrementamos su valor.

Después hemos definido la función que el operador ++ aplicará y de paso eliminamos la función Incrementar() que aunque útil, era un asco de usar. Si ahora hacemos esto: Contador_6

Contador C1(10)  ;
++C1 ;
Serial.println(C1.GetCont());

Obtendremos un bonito resultado de 11, como queríamos conseguir.

Salida consola arduino

¿Y podríamos hacer esto?

Contador C1 , C3(10) ;
C1 = ++C3 ;

Para nada, ¿ Porque? Piensalo un momento antes de seguir.

Error del compilador

Pues porque hemos definido como void el resultado del operador ++ y no podemos hacer que el resultado void, se asigne a un objeto de la Clase Contador, y naturalmente el compilador se pone atacado en cuanto lo ve.

Para resolver eso, vamos a necesitar que lo que devuelva el operador ++, sea un objeto de la Clase Contador, y para ello tenemos que definir la función así: Contador_7

Contador Contador::operator ++()
     { return Contador (++N);  }

Y ahora si que es posible hacer:

Contador C1, C3(10) ;
C1 = ++C3 ;
Serial.println(C1.GetCont());

Que aunque lo hemos hecho con mucha facilidad, conviene fijarse en un par de cosas:

 
  • De Una función puede devolver un objeto tranquilamente. Algo que hasta ahora no habíamos planteado pero que es bastante frecuente. En este caso es un objeto del tipo Contador.
  • En este caso el objeto que devolvemos es un objeto temporal que ni siquiera tiene nombre y que se calcula sobre la marcha, para devolverlo a quien invoque el operador ++.En este caso se asigna a C1 y el Objeto temporal se desvanece sin más, sin haber llegado siquiera a bautizarlo.
  • Para que este método que hemos usado funcione necesitamos haber hecho un Constructor Overloading que nos permita crear un objeto tipo y pasarle el valor que deseamos al crearlo.  

Vale, es un buen momento para tomar aire y volver a leer despacio lo de arriba, porque aunque la operación es sencilla y parece sencilla tiene un fondo importante, y de nuevo, muchos conceptos mezclados.

 

Postfix Unary Operator Overload

 

Parece que estamos haciendo un concurso de títulos raros, pero las cosas son mas o menos así.

De acuerdo, hemos hecho un Overloading del Prefix Operator, es decir, que podemos escribir ++C1 (Con el operador en modo prefijo) pero si intentáis hacerlo con el modo postfix, o sufijo: C1++, recibiréis un simpático corte de mangas del compilador, porque la sintaxis anterior describe el modo prefix pero no el suffix.

Para definir el operador suffix necesitamos usar una sintaxis un tanto extraña, pero indolora:

Contador Contador::operator ++ (int)
   { return Contador (N++);  }

Donde el int que le pasamos entre paréntesis solo significa que se refiere al postfix Operator. Es raro pero vete acostumbrando, C++ es así de maniático, y no tiene otro significado.

Ahora podemos hacer un nuevo programa Contador_8:

Contador C1, C3(10) ;
C1 = C3++ ;
Serial.println(C1.GetCont());
Serial.println(C3.GetCont());

El resultado es el que cabía esperar:

Out_2
 
  • Recordad que ++i, en prefix significa, primero incrementa y después usa el valor de i, mientras que i++, en postfix significa, que primero entregas el valor de i, y una vez que ha operado, incrementalo 

 

El Operador this

 

La solución que dimos en los últimos ejemplos, de devolver un Objeto temporal que debe ser primero creado y después destruido, funciona, (Lo que no es poco), pero tiene el inconveniente de que puede ser lento y consumir una memoria de la que rara vez estamos sobrados.

Así, que no se considera elegante, y menos para un procedimiento como devolver un Objeto, que es algo muy frecuente, y más si tenemos en cuenta, que en realidad, ya tenemos un Objeto del tipo Contador dispuesto y con el valor que queremos: C1, ¿Por qué no devolverlo directamente?

Por eso los señores que diseñan los compiladores C++ nos ofrecen una solución mucho más elegante: el operador “this”.

El operador “this” es un puntero que se pasa a disposición de todas las funciones miembro de la clase, (Y eso incluye a todas los funciones de operadores sobrecargados), que apunta al objeto al que pertenecen.

 
  • Y por eso, el compilador no necesitaba hacer copias de las funciones para todos los objetos de una clase, basta con aplicarlas apuntando a la dirección contenida en this. 

Cuando instanciamos C1, cualquier función miembro que reclame el operador this, recibe un puntero a la dirección de memoria que almacena sus datos, que por definición es una la dirección del objeto C1 (No de la definición de la clase ).

Si recordáis como trabajábamos con punteros, podremos escribir la función de Overloading del operador ++, de este modo (Coged aire): Contador_9

const Contador &Contador::operator ++()
    { ++N;
      return *this ;
    }

Antes de nadie salga corriendo, dejad que me explique. Contador_9

 
  • Definimos la función operator ++ como tipo Contador porque va a devolver un objeto de este tipo. (Esta parte ya estaba dominada, recordad)
  • La particularidad está en que avisamos al compilador con el símbolo &, de que lo que vamos a devolver es un puntero a un objeto de la clase Contador, y no un objeto.
  • Tras incrementar N, ya hemos realizado la operación que buscábamos y el objeto presente, por ejemplo C1, ya tiene el valor adecuado.
  • Y ahora devolvemos el puntero a nuestra propia instancia del Objeto con la referencia que indica el operador this y de ese modo nos ahorramos el trasiego de crear y eliminar objetos temporales.
  • Lo de especificar la función como const, es para evitar que al pasar la referencia de nuestro objeto actual, haya posibilidad de modificarlo por error. No os olvidéis de esto por si las moscas.   

Como es habitual, en cuanto se mentan los punteros, los jadeos de angustia se escuchan agónicos. Pero en serio, no os preocupéis, si ahora os resulta duro, es normal, las cosas tienen que asentarse y encontrar su sitio, así que no os agobiéis que requiere su tiempo.

Además bueno es C++ para estas cosas, pero recordad que si el tema os marea siempre podéis usar un objeto temporal que es mucho más sencillo de comprender  y sino queréis nota sobra.

En algún momento tendremos que dedicar una sesión (O varias) a las cuestiones de punteros en profundidad, porque es algo que concede a C++ una potencia sin precedentes,  pero por ahora es pronto y hay que ir poco a poco, que no quiero asustar a nadie.

 

 

Resumen de la sesión

 

 

    • Vimos la diferencia entre operadores unitarios y binarios.
    • Aprendimos a sobrecragar los operadores unitarios, tanto en prefijo como en sufijo.
    • Vimos que podemos devolver objetos como retorno de una función.
    • Presentamos el nuevo operador this.