C/C++: sobrecarga de operadores en C++

Publicado por

En el lenguaje de programación C++ existe un conjunto de símbolos especiales destinados a indicar operaciones con los tipos de datos fundamentales (enteros, reales y caracteres), estos símbolos son llamados operadores, un ejemplo es el operador de suma +” que se usa con operandos numéricos y efectúa la suma de estos, o el mismísimo operador de asignación =” que sirve para asignar los valores que tendrán las variables;  gracias a estos operadores  es posible usar expresiones como esta:

int a = 1 + 3; //asignación de la suma de 1+3 a la variable a

¿Se pueden usar los operadores en otros tipos de datos?

Todo lo anterior aplica para los tipos de datos primitivos o fundamentales de C++, es decir, dichos operadores solamente funcionan con estos operandos, pero ¿qué hay si queremos hacer suma de números complejos o de racionales? Definitivamente estos no son tipos de datos primitivos, más bien tendríamos que definirlos a partir de los existentes, tendrían que ser tipos de datos estructurados o clases definidos por nosotros. El problema es que en los compiladores de C++ no está definido como operar con números racionales o complejos porque no son tipos primitivos, por lo tanto el compilador no sabría que hacer si se encontrara con algo así:

Fraccion a(1,2); // objeto Fraccion que representa 1/2
Fraccion b(2,3);
Fraccion c;
c = a + b; // ¡Error! El operador "+" no puede operar sobre fracciones.

La respuesta a la pregunta parece ser un rotundo no, podríamos pensar entonces hacerlo a manera de funciones, tal vez algo como esto serviría:

c = suma(a,b);

Para ello hay que definir la función suma que reciba como argumentos dos fracciones (los sumandos) y retorne otra fracción (el resultado) y con esto queda resuelto el problema, pero diremos adiós a la bonita presentación que implica el uso de los operadores.

Espera, ¿que hay del operador de asignación?

Si observamos bien el ejemplo anterior, notaremos que sustituimos el operador + por suma(), pero para la asignación no hicimos algo similar, ese operador sí funciona aunque opere con objetos de la clase Fraccion, esto indica que efectivamente se pueden usar los operadores con otros tipos de datos, pero ¿por qué solo funciona con el de asignación y no con el de suma? la respuesta es muy simple: funciona con el operador de asignación porque el compilador sobrecarga el operador si el programador no lo hace explícitamente, básicamente hace una definición adicional del operador, para que pueda usar operandos del tipo Fraccion además de los tipos primitivos, de esta manera copia directamente cada atributo de un objeto a otro; esto se logra a través de un tipo especial de sobrecarga llamado sobrecarga de operadores. Este tipo de sobrecarga trabaja de manera muy similar a la convencional, también se sobrecargan funciones pero con nombres (identificadores) especiales, a estas funciones se les llama funciones-operador. La sintaxis de una función-operador es la siguiente:

// A manera de función independiente o externa
tipo_retorno operator simbolo_de_operador (tipo_arg arg1,...); //prototipo

tipo_retorno operator simbolo_de_operador (tipo_arg arg1,...){
  //Implementación de la operación
}

// Como función miembro de una clase
// Al interior de la definición de la clase
tipo_retorno operator simbolo_de_operador (tipo_arg arg1,...); //prototipo

// Implementación fuera de la definición de la clase
tipo_retorno nombre_clase::operator simbolo_de_operador (tipo_arg arg1,...){
  //Implementación de la operación
}

Lo importante aquí y diferente a la sobrecarga convencional es el uso de la palabra reservada operator seguida por el símbolo del operador que queremos sobrecargar.

Como primer ejemplo veamos la sobrecarga a manera de función independiente o externa:

// Supongamos que ya tenemos definida la clase Fraccion
#include "Fraccion.h"
// Sobrecarga del operador "+" para sumar fracciones
Fraccion operator+(const& Fraccion f1, const& Fraccion f2){
  Fraccion fr;
  fr.denominador = f1.dameDenominador() * f2.dameDenominador();
  fr.numerador = f1.dameNumerador() * f2.dameDenominador() + 
                      f2.dameNumerador * f1.dameDenominador;
  return fr;
}

Con esto ya podremos hacer sumas de fracciones  usando el operador como si sumáramos enteros, solo basta con tener definida la clase Fraccion y esta función.

Ahora veamos como podría ser la sobrecarga del operador de suma como función miembro de la clase Fraccion:

// Definición de la clase Fraccion
class Fraccion{
private:
  int numerador;
  int denominador;
public:
  Fraccion(int num=1, int den=1);
  Fraccion operator +(const Fraccion& f2);  //prototipo de la función-operador miembro
};

// Implementación de los métodos
// Constructor
Fraccion::Fraccion(int num, int den){ 
  this -> numerador = num;
  this -> den = den;
}
// Sobrecarga del operador "+" para hacer suma de fracciones
Fraccion Fraccion::operator +(const Fraccion& f2){
  Fraccion fr;
  fr.denominador = this -> denominador * f2.denominador;
  fr.numerador = this -> numerador * f2.denominador + 
                      f2.numerador * this -> denominador;
  return fr;
}

Con esta clase podemos hacer lo siguiente sin problemas:

 Fraccion a(1,2); // fracción 1/2
 Fraccion b(1,3); // fracción 1/3
 // Las siguientes dos sentencias son equivalentes
 Fraccion c = a + b; //c tendrá la fracción 5/6
 Fraccion c = a.operator+(b); //también se puede usar la notación funcional

Función-operador miembro vs función-operador independiente o externa

Ambas alternativas son buenas para resolver el problema, sin embargo tienen sus diferencias:

  • En el caso del operador como miembro de la clase solo es necesario un parámetro, ya que el objeto que use este operador será el primer argumento, a diferencia de la otra forma donde son necesarios dos objetos de Fraccion como argumentos.
  • Si implementamos el operador como parte de la clase se puede acceder a los atributos directamente aunque estos sean privados, por el contrario esto no es posible si optamos por la función independiente, aquí tendremos que usar métodos que nos retornen el valor de los atributos para poder usarlos o ponerlos como públicos aunque esto implica eliminar el encapsulamiento en la clase (una mala idea).
  • Si pensamos en que los operadores trabajan con fracciones es lógico pensar que lo mejor es dejarlos en la clase como parte de su comportamiento y tener así todo bien organizado, sin embargo, no siempre podemos modificar las clases, si somos los creadores no hay problema, pero a veces usamos el trabajo de alguien más, quien posiblemente no haya pensado en sobrecargar operadores en sus clases y no queda de otra que hacer una función independiente a la clase.
  • Existen operadores que no pueden ser sobrecargados en una función independiente, únicamente podremos hacerlo como miembros de una clase, estos operadores son los siguientes:
    • operador de asignación =”
    • operadores []” y ()”
    • operador flecha ->”

Limitaciones

Por más bonito que parezca, no todo es perfecto y es que además de las posibilidades que brinda la sobrecarga de operadores también nos impone las siguientes restricciones:

  • No es posible sobrecargar los siguientes operadores, ni siquiera como miembros de una clase:
    • operador de ámbito ::”
    • operador de acceso a miembros .”
    • operador de acceso a apuntador a miembros  .*”
    • operador ternario condicional ?:”
  • No podemos crear nuevos operadores, solo disponemos de los existentes
  • No es posible cambiar las propiedades de los operadores, como lo son la precedencia o el número de operandos que usan
  • Al sobrecargar los operadores &&” y ||” perderemos la evaluación de corto circuito asociada a las operaciones and y or

Ejemplos de la biblioteca estándar de C++

Es común que al comenzar a estudiar un nuevo lenguaje de programación se vean ejemplos muy simples de programas, el típico hola mundo” es el más popular pues no pretende mostrar características avanzadas a quien apenas comienza, sin embargo en C++ el hola mundo” nos deja ver la sobrecarga de operadores y vaya ejemplo nos da: el mismísimo std::cout que sobrecarga el operador <<” y logra una sintaxis muy llamativa para imprimir datos en pantalla, además de ver un operador en lugar de una función, este está sobrecargado para los tipos de datos primitivos e incluso para los std::string dejando todo listo para usarlo sin preocupaciones de conversiones de tipos.

Los std::string son otro claro ejemplo, aquí se sobrecarga el operador +” para realizar la concatenación y es por eso que podemos escribir líneas como estas:

std::string mensaje;
std::string saludo = "hola";
std::string receptor = " mundo";
mensaje = saludo + receptor + "!!"; //mensaje = hola mundo!!

Concluyendo

La sobrecarga de operadores es sin duda una característica muy llamativa de C++ pues además de permitirnos extender el uso de las operaciones representadas por sus operadores a más tipos de datos, también nos da oportunidad de cambiar las operaciones mismas aunque respetando las restricciones antes listadas, como el caso de la concatenación en los std::string con el operador de suma, también ofrece una vista muy elegante en nuestros programas y por si fuera poco hace posible la programación genérica al poder tener funciones miembro que se llaman igual en cualquier clase (el nombre de la función-operador) y que, con la ayuda de las plantillas (templates) pueden programarse algoritmos que funcionan con cualquier tipo de dato.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *