4.9.18 Sobrecarga de operadores
§1 Preámbulo
Hemos señalado ( 4.9) que los operadores son un tipo de tokens que indican al compilador la realización de determinadas operaciones sobre variables u otros objetos (los operandos). Por ejemplo, cuando encontramos una expresión del tipo:
z = x + y; // suponemos que x, y, z son de tipo int §1a
sabemos que la sentencia contiene dos operadores; el de suma (+) y el de asignación (=); que estos operadores actúan (operan) sobre objetos de tipo int, y que sus reglas de uso y su significado (que resultados se obtienen) están perfectamente definidos en el lenguaje.
Los operadores aceptan uno o varios operandos de tipo específico (alguno de los tipos básicos preconstruidos en el lenguaje 2.2.1), produciendo y/o modificando un valor de acuerdo con ciertas reglas. Sin embargo, C++ permite redefinir la mayor parte de ellos. Es decir, permite que puedan aceptar otro tipo de operandos (distintos de los tipos básicos ) y seguir otro comportamiento, al tiempo que conservan el sentido y comportamiento originales cuando se usan con los operandos normales. Esta posibilidad, que recibe el nombre de sobrecarga del operador, es una consecuencia del polimorfismo y hace posible que en la expresión:
c = d + e; // suponemos que c, d, e son de tipo UnaClase §1b
el sentido de la suma y el de asignación sean totalmente específicos y distintos que el de la expresión §1a anterior cuando se utilizaron los mismos operadores con operandos tipo int.
La primera e importantísima advertencia es que la sobrecarga se refiere y tiene aplicación solo cuando los operandos son instancias de clases. Es decir, no es posible modificar el sentido de la suma y asignación de la expresión §1b si c, d y e fuesen enteros o de cualquiera de los tipos básicos preconstruidos en el lenguaje.
Nota: aunque la sobrecarga de operadores se haya incluido en la sección dedicada a los operadores, recomendamos al principiante que se salte el resto de este capítulo hasta no tener un buen conocimiento de las clases ( 4.11). Este conocimiento le resultará imprescindible para comprender las particularidades de la sobrecarga de operadores, que se aplica cuando los operandos son instancias de clases y se define mediante funciones en las propias clases.
§2 Permanencia de las leyes formales
El lenguaje C++ no impone ninguna restricción en la forma que adopte la sobrecarga de un operador. De forma que se puede conseguir que incluso operadores básicos como la suma (+), la asignación (=) o la identidad (==) adquieran para los tipos abstractos ( 2.2) un carácter totalmente distinto del que adoptan para los tipos básicos. No obstante, lo normal, lógico, y aconsejable, es mantener la máxima homogeneidad conceptual en el polimorfismo ( 1.1). Es decir, que el efecto básico de los operadores (la imagen mental de su significado) se mantenga, aunque su implementación concreta varíe de una clase a otra. Por ejemplo, las definiciones de suma (+), asignación (=) e identidad (==) para los elementos de una clase C deberían garantizar que después de ejecutada la sentencia c = d; sobre instancias c y d de dicha clase, el resultado de (c == d) fuese true. El resultado de aplicar el constructor-copia para crear un objeto a partir de otro debería producir un nuevo objeto igual que el modelo, Etc.
Otro aspecto que debería mantenerse, es que los operadores que normalmente no tienen efectos laterales ( 4.9), se mantengan igualmente libres de tales efectos en sus versiones sobrecargadas [5]. Lo anterior puede expresarse con otras palabras: debe procurarse que las propiedades formales de los operadores matemáticos se mantengan también en la versión sobrecargada. Por ejemplo, que la suma sea conmutativa y asociativa, que la identidad sea simétrica y transitiva, Etc. Al tratar la sobrecarga de los operadores relacionales ( 4.9.18b1) se abunda en estos conceptos.
§3 Sinopsis
A excepción de los que se detallan , el lenguaje C++ permite la sobrecarga los operadores estándar (). La nueva versión del operador se diseña de forma que presente un comportamiento especial cuando los operandos sean instancias de clase. Por ejemplo, el operador de identidad == podría ser definido en una hipotética clase Complejo para verificar la identidad de dos números complejos, al mismo tiempo que mantendría su uso normal cuando se utilizara con tipos básicos (int, float, char, Etc).
Para distinguir unas de otras, a las versiones de los operadores preconstruidas en el lenguaje las denominamos versiones globales, mientras que a las definidas por el usuario, versiones sobrecargadas.
§3.1 Operadores sobrecargables
El lenguaje C++ permite redefinir la funcionalidad de los siguientes operadores:
+ - * / % ^ &
| ~ ! = < > +=
-= *= /= %= ^= &= |=
<< >> >>= <<= == != <=
>= && || ++ -- ->* ,
-> [] () new new[] delete delete[]
Los operadores +, -,
* y & son
sobrecargables en sus dos versiones, unaria y binaria. Es decir: suma binaria +
( 4.9.1); más unitario
+ (
4.9.1); multiplicación *
( 4.9.1);
indirección *
( 4.9.11);
referencia &
( 4.9.11)
y manejo de bits &
( 4.9.3).
Es notable que C++ ofrece casos de operadores sobrecargados incluso en su Librería Estándar. Por ejemplo, los operadores == y != para la clase type_info ( 4.9.14a). Sin embargo, la posibilidad de sobrecarga no se extiende a todos los operadores (ver a continuación las excepciones ).
§4 Limitaciones:
La sobrecarga de un operador no puede cambiar el número de operandos o la asociatividad y precedencia del mismo (4.9.0a). En otras palabras: se puede modificar su funcionalidad pero no su gramática original. Por ejemplo, un operador unario no puede ser transformado en binario y viceversa.
- Los operadores globales no pueden ser sobrecargados.
- No pueden definirse nuevos tokens como operadores. En caso necesario deben utilizarse funciones. Por ejemplo, no puede definirse ** como un token para representar la exponenciación (a ** b); en todo caso utilizar algo así: pow(a, b).
No es posible redefinir el sentido de un operador aplicado a un puntero. Por ejemplo, no es posible modificar el sentido del operador suma (+) entre un puntero y un entero (sentencia L.3).
class CL { /* ... */ };
CL c1, *cpt1 = &c1;
CL* cpt2 = cpt1 + 5; // L.3En otras palabras: no es posible modificar la aritmética de punteros ( 4.2.2) sobrecargando sus operadores [3].
§5 Excepciones:
En la lista anterior (§3.1 ), puede verificarse que todos los operadores pueden ser sobrecargados, incluyendo new, new[ ], delete y delete[ ], excepto los siguientes:
§5.1 Los operadores asignación =
( 4.9.2); elemento de matriz
[ ] (
4.9.16); invocación de función ( ) ( 4.9.16)
y selector indirecto de miembro -> (
4.9.16) pueden ser sobrecargados solamente como
funciones-miembro no estáticas
( 4.11.7) y no pueden ser sobrecargados
para las enumeraciones (4.7). Cualquier
intento de sobrecargar la versión global de estos operadores produce un error de compilación.
§5.2 Con la excepción de los anteriores ( =, [ ], ( ) y ->), los operadores también pueden ser sobrecargados para las enumeraciones [4] ( 4.8) y pueden utilizarse funciones-miembro estáticas.
Nota: Incluso en algunos casos, como en la sobrecarga de los operadores new y delete mediante funciones-miembro, los métodos operator new( ) y operator delete( ) son declarados estáticos por defecto [6].
§5.3 La sobrecarga del operador de invocación de función
( )
conduce a un tipo función-miembro, que goza de una sintaxis de invocación especial (Objeto-función
4.9.18f,
5.1.3a1).
§6 La función-operador
Ya hemos señalado ( 4.9), que los operadores C++ pueden considerarse funciones con identificadores un tanto especiales. Por ejemplo, cuando tenemos el operador de subíndice de matriz, x[y], donde x es un objeto de la clase X, el compilador lo traduce a la expresión: x.operator[](y). Es decir, lo interpreta como la invocación de un método de nombre operator[ ].
Este comportamiento del compilador puede hacerse extensivo al resto de operadores, de forma que, cuando se trata de miembros de clases, si @ representa un operador binario, la expresión a @ b es en realidad una forma abreviada de representar la invocación de una función-miembro (método): a.operator@(b), o de una función externa equivalente: operator@(a, b) [2].
Igualmente, si @ representa un operador unario (por ejemplo, el operador preincremento ++ 4.9.1), la expresión @ a es la forma abreviada de representar la invocación de una función-miembro que no acepte argumentos: a.operator@( ), o de una función externa equivalente que acepte un argumento: operator@(a).
Estas funciones, denominadas función-operador, determinan el tipo de los operandos; el Lvalue y orden de evaluación que se aplicará cuando se utilice el operador. Como consecuencia, la sobrecarga de un operador se realiza bajo la forma de sobrecarga de la función-operador y su definición determinará el nuevo comportamiento. Como en el caso general de sobrecarga de funciones, el compilador distinguirá las diferentes funciones-operador por el contexto de la llamada (número y tipo de los argumentos).
Los puntos a resaltar aquí son que los operadores @ susceptibles de sobrecarga son equivalentes a la invocación de una función operator@ que (con algunas excepciones) puede ser de dos tipos: una función-miembro no-estática de la clase, o una función externa. En uno y otro caso la función adopta distinta forma. El cuadro adjunto muestra esta relación de equivalencia (puede verse que hay tres operadores que solo aceptan la forma de función-miembro).
Expresión | Función-miembro | Función externa |
@a | a.operator@() | operator@(a) |
a@b | a.operator@(b) | operator@(a,b) |
a=b | a.operator=(b) | |
a[b] | a.operator[](b) | |
a-> | a.operator->() | |
a@ | a.operator@(0) | operator@(a,0) |
En aquellos casos en que un operador puede ser sobrecargado de dos formas: como función externa y como función-miembro, no deben utilizarse simultáneamente ambas formas para un mismo operador. El resultado sería impredecible y posiblemente catastrófico.
Tenga en cuenta que las versiones unarias y binarias de un operador comparten el mismo nombre de función. Por ejemplo, operator* es el nombre de la función-operador de la indirección y la multiplicación. De forma que a.operator*() es una forma del operador de indirección (equivale a *a), mientras que a.operator*(b) representa al operador de multiplicación (equivale a a*b).
§6.1 La palabra clave operator seguida del símbolo del operador conforma el identificador de la
función-operador. Ejemplos:
<tipo-devuelto> operator + (/*...*/) {/*...*/} ;
<tipo-devuelto> operator [] (/*...*/) {/*...*/} ;
<tipo-devuelto> operator - (/*...*/) {/*...*/} ;
<tipo-devuelto> operator ->* (/*...*/) {/*...*/} ;
<tipo-devuelto> operator = (/*...*/) {/*...*/} ;
<tipo-devuelto> operator= (/*...*/) {/*...*/} ;
Es indiferente dejar un espacio entre la palabra operator y el símbolo del operador (las dos últimas líneas son equivalentes). Además la identificación operador ↔ función-operador no es solo interna, también puede ser utilizada explícitamente en el código. Una función-operador invocada con los argumentos apropiados, se comporta en cualquier sentencia como un operador con sus operandos. Por ejemplo:
UnaClase c1, c2, c3;
...
c2 = c1; // L.3: Ok. asignación
c2.operator=(c1); // L.4: Ok. la misma asignación
c3 = c1 + c2; // L.5: suma y asignación
c3.operator=(c1.operator+(c2)); // Ok. equivalente a L.5:
Las sentencias L.3 y L.4 son equivalentes. Aunque legal, la expresión L.4 no es la forma usual de invocar al operador de asignación =.
§6.2 La definición de la nueva acción (sobrecargada) del operador se realiza como con cualquier función normal.
Ejemplo:
class Complex {
...
Complex& operator[](unsigned int i) { return data[i]; } //
definición inline
...
}
Complex& Complex::operator[](unsigned int i) { return data[i]; } // def. offline
En este caso estamos sobrecargando el operador elemento de matriz [ ] como función que acepta un entero como
argumento. Se establece el tipo de argumento a utilizar y que la función devuelve una referencia a un objeto de la clase
Complex. Como en el resto de funciones, la acción se concreta en el cuerpo delimitado por los corchetes { }.
§6.3 La función-operador no puede utilizar argumentos por defecto salvo en los casos que se autorizan
expresamente. Tampoco pueden tener más o menos argumentos que los indicados en cada caso
(ver tabla ).
§6.4 La función-operador puede ser miembro o friend (función externa) de la clase para la que se
define. Que se utilice una u otra forma es, a veces, cuestión de preferencia personal, pero en otras viene obligada
[2]. En cualquier caso, las
funciones-operador son buenas candidatas para ser declaradas funciones inline
( 4.11.2a).
Se suelen declarar miembros de la clase los operadores unarios (de un solo operando), o los que modifican el primer operando (caso de los operadores de asignación). En estos casos el primer operando debe de ser necesariamente una instancia de esa clase, en concreto el objeto que constituye el argumento implícito ( 4.11.6). Por esta causa, salvo que sean declaradas funciones estáticas [1], puesto que el puntero this es incluido de forma implícita en la declaración, sólo hará falta incluir el segundo operando en la lista de argumentos de la función-operador.
Se suelen declarar friend los operadores que aceptan varios operandos sin modificarlos (por ejemplo los operadores aritméticos y lógicos). En estos casos se exige que al menos uno de sus operandos (argumentos) sea del tipo de la clase para la que se define (las funciones-operador que redefinen los operadores new y delete son la excepción de esta regla).
§7 Ejemplo
En el ejemplo que sigue se define una clase Vector con dos miembros (float) y se sobrecarga el operador suma (+) para que funcione sobre objetos de esta clase. La función devuelve un objeto de la clase y el único argumento también es un objeto de tipo Vector.
class Vector { // definición de la clase Vector
public:
float x, y;
Vector operator+ (Vector v) { // función-operador operator+
Vector resultado;
resultado.x = x + v.x;
resultado.y = y + v.y;
return resultado;
}
};
Como puede verse, la función operator+ devuelve un objeto de la clase que es suma miembro a miembro de los elementos del vector pasado como argumento. En principio este diseño permitiría expresiones del tipo v1 + v2.
§8 Herencia y sobrecarga de operadores
A excepción del operador de asignación simple = ( 4.9.18a), todas las funciones-operador sobrecargadas en una clase antecesora son heredadas en las clases derivadas. Si B es base de la clase D, un operador @ sobrecargado para B puede ser sobrecargado más tarde para D. Es decir, pueden coexistir las siguientes definiciones:
B::operator@( ){ /* definicion para super-Clase */ }
D::operator@( ){ /* definicion para clase derivada */ }
Ver detalles complementarios sobre operadores y jerarquías de clases ( 4.9.18t).
§9 Pertinencia de la sobrecarga de operadores
Algunos manuales de estilo recomiendan enérgicamente evitar la sobrecarga de operadores; otros la admiten en caso de utilizarse sobre estructuras matemáticas del tipo de complejos, vectores, matrices, etc.
Su utilización descuidada puede ser sin duda origen de confusiones, pero ¿Qué puede no serlo en C++?. Además, es innegable que en ocasiones puede ser un recurso utilísimo y elegante. En cualquier caso, la posibilidad está ahí, para ser usada a criterio del programador. Al final, utilizarla o no, seguramente sea una cuestión de mera preferencia personal.
Tema relacionado
- Búsqueda en el espacio de los argumentos ( 4.1.11c).
[1] Como hemos visto , ciertas funciones-operador no pueden ser estáticas por definición del propio lenguaje.
[2] No siempre es posible sobrecargar mediante una función externa. Ya hemos señalado que algunos operadores solo pueden ser sobrecargados bajo la forma de funciones-miembro (métodos).
[3] Como ocurre con la mayoría de las limitaciones del lenguaje, existen técnicas que permiten solventarla, de forma que es posible simular esta sobrecarga ( 4.9.18c).
[4] Observe que esta afirmación parece estar en contradicción con lo indicado al principio , que "la sobrecarga se refiere y tiene aplicación solo cuando los operandos son instancias de clases". Sin embargo hay que tener en cuenta que las enumeraciones son en realidad un tipo muy particular de estructuras, y por ende, de clases. Como tales, gozan de muchas de las características de aquellas.
[5] Esta ausencia de efectos laterales se refiere al ámbito de las operaciones involucradas, con independencia de que puedan obtenerse otros efectos auxiliares (estadísticos o de cualquier tipo).
[6] B. Stroustrup TC++PL §15.6: "Member operator new () and operator delete () are implicitly static members".