Objetivos

 

  • Sobrecarga de operadores binarios.
  • Ejemplo de sobrecarga con operadores + y >
  • Conversión de tipos de datos.
  • Algunas muestras de conversiones automáticas. .   
  •  

    Material requerido.

    Imagen de Arduino UNO Arduino Uno o similar.

     

    Paz Interior Mucha paz interior

    Overloading Binary Operators

     

    En la sesión previa vimos que eran y como hacer la sobrecarga de operadores unitarios, aquellos que se aplicaban a un único operando, como el incrementar y decrementar, por ejemplo. En esta sesión vamos a ver cómo hacemos para programar la sobrecarga de operadores binarios, aquello que como la suma +, o el producto *, involucran dos operadores para producir un resultado.

    Vamos a partir de la Clase Contador, que llevamos mareando un tiempo, porque es una clase lo bastante sencilla para evitar complicaciones innecesarias y porque además es un ejemplo excelente, por lo simple, de lo que se puede hacer con una Clase, sin que el programa de ejemplo se estire hasta un punto en el que sea difícil de seguir la idea.

    La historia hasta aquí:  La definición de la Clase Contador:

    class Contador
      {   private:
             int N ;
     
          public:
             Contador( ) : N(0) {}                    // Constructor
             Contador(int k ) : N(k) {}               // Constructor
             void SetContador( int n) ;
             int GetCont() ;
             const Contador &operator ++ ();
      } ;
    
    void Contador::SetContador( int n)
            {  N = n ;    }
    
    int Contador::GetCont()
            { return (N) ;}
    
    const Contador &Contador::operator ++()   // Prefix Operator
             { ++N;
               return *this ;
             }
    Contador Contador::operator ++ (int)      // Postfix Operator
             { return Contador (N++);  }

    Ya disponemos en la clase, de dos Constructores diferentes, uno para inicializar a 0 se crea sin parámetros y otro que inicializa el contador interno en el caso de que se le suministre un valor.

    Pero imagínate que queremos sumar contadores. ¿Por qué no? Podemos sumar los registros internos de dos contadores, de modo que el resultado sea otro contador con un valor de N interno igual a la suma de los dos operandos.

    No sería complicado definir una función miembro, que podemos llamar Suma, que devuelva un objeto Contador tras operar con dos contadores. Podríamos hacer algo así:

    Contador  Contador::Suma( const Contador & C1 ) 
          {   return Contador  ( N + C1.GetCont() ) ; }

    Que resulta muy fácil de escribir, pero un poco más pesado de digerir. La función miembro Suma toma una referencia  a un Contador  C1 genérico, (Forzada a const para evitar sustos)  y devuelve un objeto tipo Contador con el resultado de sumar ambos valores internos (Sé que en cuanto hay unareferencia la cosa se complica y se escuchan gemidos).

     

  • Para que esto funcione, tenemos que disponer de un Constructor que acepte crear un objeto de este tipo mediante la operación de la segunda línea, pero esto ya lo teníamos definido.  
  •  

    Aunque este método funcionará, su uso es más bien repelente :

    Contador C1(), C2(23) ;
    Contador C3 = C1.Suma(C2) ;

    No tiene ningún problema, es simplemente que recordar esto es un asco y fácil de olvidar si la función era suma, Suma, Add o ….

    Aquí somos gente elegante y lo que queremos hacer es algo más intuitivo y que no se nos olvidará nunca. Queremos escribir la suma así, como corresponde a cualquier programador que se precie:

    Contador C3 = C1 + C2 ;

    Mucho más presentable y elegante, ¿No? Y para eso está la sobrecarga de operadores binarios, como el +. Es tan fácil de hacer como cualquiera de los ejemplos previos, y sería algo así mediante el uso de la clave operator como ya vimos:

    class Contador
      {   private:
             int N ; 
    
          public:
             Contador( ) : N(0) {}                 // Constructor
             Contador(int k ) : N(k) {}            // Constructor
             void SetContador( int n) ;
             int GetCont() ;                 
             const Contador &operator ++ ();
             Contador operator ++ (int) ;
             Contador operator + (  Contador &) ;  // Pasamos una referencia a un contador
      } ;

    Obviando las funciones que ya tenemos claras la novedad seria: Contador_10

          Contador  Contador::operator+ (  Contador & C1 )
               { return Contador  (  N + C1.GetCont() ) ; }

    Ahora podemos hacer esto tranquilamente:

    void loop()
       {    Contador C1, C2(10), C3(11) ;
            C1 = C2 + C3 ;
    
            Serial.println(C1.GetCont());
            Serial.flush();   exit(0);
       }

    Con este resultado:

    Muestra resultado

     

    Comparator Overload

     

    Podemos sobrecargar más operadores, por ejemplo el operador ‘>’ para comparar dos Contadores y realizar alguna operación en consecuencia. No parece descabellado hacer una comparación parecida a esto: Contador_10B

    Contador C1, C2(12) ;
    if ( C1 > C2 )
         Serial.println( “Mayor”);
    else
         Serial.println( “Menor”);

    Si lo intentamos por las buenas C++ estará encantado de darnos un corte de mangas e informarnos que no tiene ni pastelera idea de cómo usar este operador con objetos tipo Contador. Pero podemos especificárselo así:

    bool operator > (Contador Cont)
       {    if ( N > Cont.N)
                 return true ;
            else
                 return false ;
       }

    Y ahora sí, nos permitirá hacer :

    Contador C1 = 12, C2 = 6  ;
    if (C1 > C2 )
        Serial.println("SI");
    else
        Serial.println("NO");

    Aquí tenéis el resultado:

    Resultado externo

    Este es un momento tan bueno como cualquier otro para indicar que además de operadores unitarios y binarios existe un operador terciario (¿O trinario?) y para ilustrar su ejemplo podemos escribir la función anterior así:

    bool operator > (Contador Cont)
       { return  ( N > Cont.N ) ? true : false ;  }

    Para ser franco, aunque es un modo compacto de escribir condicionales, no os lo recomiendo porque va a despistar mucho a más de uno que tenga que leer el código, pero por si acaso os hago la anotación porque encontrareis, que quienes escriben las clases parecen tener un gusto especial por escribir cosas de este modo.

     

    Consideraciones sobre la sobrecarga de operadores

     

    No pretendemos en esta humilde introducción a la Programación Orientada a Objetos con C++, hacer un repaso exhaustivo de todos los operadores que se pueden sobrecargar, sino simplemente ver una primera aproximación con algunos ejemplos sencillos.

    Por ejemplo, no sería complicado definir el operador – para hacer la resta de contadores y si os parece os lo dejo como ejercicio, pero solo aspiramos  a daros una primera visión de porque es interesante hacer Operator y función Overloading en vuestros programas.

    Aunque ahora os pueda parecer increíble, cuando empecéis a jugar con Clases, veréis que la sobrecarga viene sola a vuestros programas a poco que entendáis la idea, y es algo que simplifica mucho la comprensión de los programas.

    De hecho el problema nos es que no la uséis, sino que los nuevos tienden a usarla en exceso y para cosa que no se deberían hacer.

    Por ejemplo sería muy fácil redefinir el operator -, para hacer sumas pero naturalmente sería estúpido y exasperarías a quien fuera a usarlo. Mantened la sobrecarga de operadores en un límite sensato y aplica la regla de que si dudas de si algo es sensato, entonces seguro que no lo es.

    Cuando programes ten piedad de quien tenga que leer tu código (Que probablemente serás tú además) y utiliza la sobrecarga para hacer los programas más fáciles de leer, no para impresionar a tus colegas. Se trata de evitar leerse el manual y que la lectura del código se comprenda de modo natural.

    En el ejemplo de suma que hemos hecho arriba a cualquiera sin leer un manual se le puede ocurrir intentar la suma del modo que lo hemos hecho.

     

    Conversión de tipos de datos

     

    Hay un tema muy relacionado con los operator overloading que tiene que ver con la conversión de tipos en nuestros programas. Veamos porque.

    Con lo visto hasta ahora podemos crear un objeto tipo contador directamente con o sin parámetro inicial, pero se nos puede ocurrir con facilidad  preguntarnos si podríamos hacer algo como esto:

    Contador C1 = 5 ;

    Al primer bote, parece razonable. Nos ahorraríamos la función SetContador() que aunque útil, es poco practica por las mismas razones que dimos arriba, hay que sabérsela  y no es evidente, pero la línea de aquí arriba seria de lo más fácil de entender, y nos acordaríamos seguro.

    Pero… ¿Funcionaría? Después de todo, estamos forzando una conversión de un tipo int a un tipo Contador, y no hemos especificado como hacer esta conversión. Si lo intentáis obtendremos esto: Contador_11

    Resultado programa

    C++ nos vuelve a sorprender con una conversión automática y un buen ejemplo de cómo intenta entendernos, ¿Pero cómo ha sabido hacer la conversión de tipo?

    La razón estriba en que ha supuesto que queremos convertir un int a Contador y es lo bastante astuto para buscar un constructor que requiera un único int como argumento, y lo ha encontrado. Por eso ha sabido cómo hacer una conversión de tipo automática.

    O dicho de otro modo ha supuesto, mediante la firma de la función sobrecargada lo que pretendíamos, y lo ha aceptado sin rechistar, pero tened cuidado con las conversiones automáticas porque pueden daros más de una sorpresa.

    Y para mayor sorpresa aun, esto otro también va a funcionar (por increíble que parezca) con un float:

    Contador C1 = 5.7F ;

    ¿Pero cómo es posible? No existe un constructor a partir de un float.

    Pues porque cuando C++ vea que le pasmaos un float, hará una conversión automática de float a int ( Incluida en las conversiones automáticas de tipos) cortando la parte decimal y asignándoselo al Constructor con int de la Clase, y esto no es algo  que pudieramos esperar, pero la vida está llena de sorpresas, ya ves.

    Por tanto y en contra de todo pronóstico (Y con más de un ataque de nervios de cualquier teórico de los lenguajes de programación) la función:

    void SetContador( int n) ;

    Puede tranquilamente ser borrada sin consecuencias de nuestras funciones miembros, ya que el constructor mediante int es capaz de hacer su labor de modo automático. ( Si querían elegancia aquí hay taza y media)

    Y es que quien va a querer recordar el nombre de una función, cuando un sencillo símbolo de asignación es suficiente , es lo primero que cualquiera probaríamos para asignar valor al contador.

    ¿Y al revés funcionaria? Me refiero a si puedo hacer:

    Contador C1 = 12 ;
    int i = C1 ;

    El compilador se pone atacado inmediatamente y nos dice que no tiene ni idea de cómo convertir un objeto tipo Contador en un int, lo que no resulta sorprendente.

    Arduino

    Pero C++ dispone de un mecanismo para definirle esta conversión, y que en un caso tan sencillo como nuestra clase Contador,  parece que lo lógico sería asignar al int el valor interno del contador.

  • Mucho cuidado con estas conversiones que no siempre son tan evidentes y la cosa se puede complicar, pero aquí estamos para presentaros un ejemplo sencillo de algo que os puede hacer falta.  
  • Para especificarle al compilador que queremos asignar a un int el valor contenido en N, podemos usar la instrucción :

    operator unsigned int()
        { return (N) ;   }

    Donde os conviene fijaros en que la sintaxis es un poco extraña. Las conversiones de tipos no se declaran con tipo de retorno, aunque devuelva uno (A mí no me miréis)

    Y ahora nuestro programa ha definido al compilador como convertimos de int a Contador (Mediante un Constructor) y como convertir a int un Contador, ¿Qué os parece?    Contador_12

    Contador C1 = 12 ;
    int i = C1 ;
    
    Serial.println(i);

    La salida es esta:

    Resultado calciulo

    No puedo resistirme aquí a una pequeña maldad, que consiste en que probéis algo aparentemente inocente. Con este mismo programa haced lo siguiente: Contador_13

    Contador C1 = 12, C2 = 6  ;
    if (C1 > C2 )
        Serial.println("SI");
    else
        Serial.println("NO");

    Tal y como vimos en algún programa anterior la respuesta es, tal y como esperábamos:

    Resultado externo

    Nada de particular en esto ¿Noo?

    El problema radica en que en este programa no hemos definido el operador > por ningún sitio. Comprobadlo sino me creéis:

    class Contador
       {  private:
             int N ;
    
          public:
             Contador( ) : N(0) {}                    // Constructor
             Contador(int k ) : N(k) {}               // Constructor
             int GetCont() ;
             const Contador &operator ++ ();
             Contador operator ++ (int) ;
             Contador operator + (  Contador &) ;   // Pasamos una referencia a un contador
             operator unsigned int()
                 { return (N) ;   }
       } ;

    ¿Pero entonces cómo es posible que funcione?

    De nuevo la respuesta está en las suposiciones con las que C++ trata de ayudarnos que van normalmente en la buena dirección pero que os pueden jugar malas pasadas si no estáis sobre aviso.

    Como C++ sabe cómo convertir nuestros Contadores a una variable tipo int que conoce bien, ha supuesto que podría hacer la comparación así, en tanto en cuanto no le declaremos específicamente otra forma de hacer la conversión.

     

  • Naturalmente, si le quitas la conversión de Contador a int, la comparación sobreentendida dejara de funcionar.  
  • Así pues tened un poco de cuidado hasta que os acostumbréis, porque algunas de estas suposiciones pueden acabar siendo un tiro en el pie.

     

    Resumen de la sesión

     

  • Vimos  cómo sobrecargar los operadores binarios.
  • Hicimos un par de ejemplos con + y >.
  • Presentamos las conversiones automáticas de tipos.
  • Vimos como declarar conversiones de tipos en nuestras propias clases.