Sobrecarga de operadores
Personaliza los operadores de C++ para los operandos de tipos definidos por el usuario.
[editar] Sintaxis
Los operadores sobrecargados son funciones con nombres de función especiales:
operator op
|
(1) | ||||||||
operator tipo
|
(2) | ||||||||
operator new operator new []
|
(3) | ||||||||
operator delete operator delete []
|
(4) | ||||||||
operator "" sufijo-de-ident
|
(5) | (desde C++11) | |||||||
operator co_await
|
(6) | (desde C++20) | |||||||
op | - | cualquiera de los siguientes 38 (hasta C++20)39 (desde C++20) operadores:+ - * / % ^ & | ~ ! = < > += -= *= /= %= ^= &= |= << >> >>= <<= == != <= >= <=> (desde C++20) && || ++ -- , ->* -> ( ) [ ] |
[editar] Operadores sobrecargados
Cuando un operador aparece en una expresión, y al menos uno de sus operandos tiene un tipo de clase o un tipo de enumeración, entonces se utiliza la resolución de sobrecarga para determinar la función definida por el usuario a ser llamada de entre todas las funciones cuyas signaturas coinciden con lo siguiente:
Expresión | Como función miembro | Como función no miembro | Ejemplo |
---|---|---|---|
@a | (a).operator@ ( ) | operator@ (a) | !std::cin llama a std::cin.operator!() |
a@b | (a).operator@ (b) | operator@ (a, b) | std::cout << 42 llama a std::cout.operator<<(42) |
a=b | (a).operator= (b) | no puede ser no-miembro | Dado std::string s;, s = "abc"; llama a s.operator=("abc") |
a(b...) | (a).operator()(b...) | no puede ser no-miembro | Dado std::random_device r;, auto n = r(); llama a r.operator()() |
a[b] | (a).operator[](b) | no puede ser no-miembro | Dado std::map<int, int> m;, m[1] = 2; llama a m.operator[](1) |
a-> | (a).operator-> ( ) | no puede ser no-miembro | Dado std::unique_ptr<S> p;, p->bar() llama a p.operator->() |
a@ | (a).operator@ (0) | operator@ (a, 0) | Dado std::vector<int>::iterator i;, i++ llama a i.operator++(0) |
en esta tabla, |
Además, para los operadores de comparación ==, !=, <, >, <=, >=, <=>, la resolución de sobrecarga también considera los candidatos reescritos generados a partir de operator== u operator<=>. |
(desde C++20) |
Nota: para sobrecargar co_await
, (desde C++20)funciones de conversión definidas por el usuario, literales definidos por el usuario, asignación de memoria y desasignación de memoria véanse sus artículos respectivos.
Los operadores sobrecargados (pero no los operadores integrados) pueden llamarse usando la notación de función:
std::string str = "Hola, "; str.operator+=("mundo"); // lo mismo que str += "mundo"; operator<<(operator<<(std::cout, str) , '\n'); // lo mismo que std::cout << str << '\n'; // (desde C++17) excepto por el secuenciado
[editar] Restricciones
- Los operadores
::
(resolución de ámbito),.
(acceso a miembro),.*
(acceso a miembro a través de puntero a miembro), y?:
(condicional ternario) no pueden sobrecargarse. - No se pueden crear nuevos operadores, tales como
**
,<>
, o&|
. - Las sobrecargas de los operadores
&&
y||
pierden su evaluación de cortocircuito. - La sobrecarga del operador
->
debe devolver ya sea un puntero sin formato, o devolver un objeto (por referencia o por valor) para el que el operador->
está a su vez sobrecargado. - No es posible cambiar la precedencia, el agrupamiento, o el número de operandos de los operadores.
|
(hasta C++17) |
[editar] Implementaciones canónicas
Aparte de las restricciones anteriores, el lenguaje no impone otras restricciones sobre lo que hacen los operadores sobrecargados, o sobre el tipo de retorno (no participa en la resolución de sobrecarga), pero en general, se espera que los operadores sobrecargados se comporten de la manera más similar posible operadores incorporados: se espera que operator+ agregue, en lugar de multiplicar sus argumentos, se espera que operator= asigne, etc. Se espera que los operadores relacionados se comporten de manera similar (operator+ y operator+= realizan la misma operación de adición). Los tipos de retorno están limitados por las expresiones en las que se espera que se use el operador: por ejemplo, los operadores de asignación devuelven por referencia para que sea posible escribir a=b=c=d, porque los operadores integrados lo permiten.
Los operadores sobrecargados tienen las siguientes formas típicas, canónicas:[1]
[editar] Operador de asignación
El operador de asignación (operator=) tiene propiedades especiales: véase asignación de copia y asignación de movimiento para más detalles.
Se espera que el operador de asignación canónico no realice ninguna acción durante la autoasignación, y devolver lhs por referencia:
// suponer que el objeto alberga almacenamiento reutilizable, // tal como un búfer mArray asignado del montículo T& operator=(const T& other) // asignación de copia { if (this != &other) { // se espera comprobación de autoasignación if (other.size != size) { // almacenamiento no puede ser reutilizado delete[] mArray; // destruir almacenamiento en this size = 0; mArray = nullptr; // conservar invariantes en cas de que la // próxima línea lance una excepción mArray = new int[other.size]; // crear almacenamiento en this size = other.size; } std::copy(other.mArray, other.mArray + other.size, mArray); } return *this; }
Se espera que la asignación de movimiento canónica deje el objeto que ha sido movido en un estado válido (es decir, con sus invariantes de clase intactas), y bien no haga nada o al menos deje el objeto en un estado válido durante la autoasignación, y devuelva lhs por referencia no const
, y que sea noexcept
:
T& operator=(T&& other) noexcept // asignación de movimiento { if(this != &other) { // no-op durante autoasignación de movimiento // (delete[]/size=0 también está bien) delete[] mArray; // eliminar este almacenamiento mArray = std::exchange(other.mArray, nullptr); // dejar objeto que ha sido movido // en un estado válido size = std::exchange(other.size, 0); } return *this; }
En aquellas situaciones en las que la asignación de copia no puede beneficiarse de la reutilización de recursos (no gestiona un array asignado del montículo y no tiene un miembro (posiblemente transitivo) que sí lo tenga, como un miembro std::vector o std::string), existe una abreviación conveniente y popular: el operador de asignación de copiar e intercambiar, que toma su parámetro por valor (por lo tanto, funciona como asignación de copia y movimiento según la categoría de valor del argumento), intercambia con el parámetro y deja que el destructor lo limpie.
T& T::operator=(T arg) noexcept // constructor de copia/movimiento se llama para construir arg { std::swap(size, arg.size); // recursos se intercambian entre *this y arg std::swap(mArray, arg.mArray); return *this; } // se llama al destructor de arg para liberar los recursos anteriormente albergados por *this
Esta forma automáticamente proporciona la garantía de excepción fuerte, pero prohíbe la reutilización de recursos.
[editar] Extracción e inserción de flujo
Las sobrecargas de operator>>
y operator<<
que toman un std::istream& o un std::ostream& como el argumento del lado izquierdo se conocen como operadores de inserción y extracción. Ya que toman un tipo definido por el usuario como el argumento del lado derecho (b
en a@b), deben implementarse como no miembros.
std::ostream& operator<<(std::ostream& os, const T& obj) { // escribir obj al flujo return os; } std::istream& operator>>(std::istream& is, T& obj) { // leer obj del flujo if( /* T no pudo construirse */ ) is.setstate(std::ios::failbit); return is; }
A veces estos operadores se implementan como funciones amigas.
[editar] Operador de llamada a función
Cuando una clase definida por el usuario sobrecarga el operador de llamada a función, operator(), se convierte en un tipo FunctionObject.
Un objeto de tal tipo puede usarse en expresiones similares a llamadas a función:
// Un objeto de este tipo representa una función lineal de una variable a*x + b struct Linear { double a, b; double operator()(double x) const { return a*x + b; } }; int main() { Linear f{2, 1}; // representa la función 2x + 1 Linear g{-1, 0}; // representa la función -x // f y g son objetos que pueden usarse como una función double f_0 = f(0); double f_1 = f(1); double g_0 = g(0); }
Lo siguiente son dos alternativas para tal enfoque que no utilizan sobrecarga pero tienen desventajas.
1. Uso de variables globales:
double a, b; // Malo: variables globales. double linear(double x) { return a*x + b; } int main() { a = 2; b = 1; double f_0 = linear(0); double f_1 = linear(1); // Malo: se necesita reasignar los parámetros para poder // calcular una función distinta: a = -1; b = 0; double g_0 = linear(0); }
2. Uso de parámetros adicionales:
double linear(double a, double b, double x) { return a*x + b; } int main() { double f_0 = linear(2, 1, 0); // Malo: tienen que repetirse nuevamente los mismos parámetros: double f_1 = linear(2, 1, 1); double g_0 = linear(-1, 0, 0); }
Varios algoritmos estándar, desde std::sort hasta std::accumulate aceptan FunctionObjects para personalizar su comportamiento. No existen formas canónicas particularmente notables de operator(), pero para ilustrar el uso:
struct Sum { int sum; Sum() : sum(0) { } void operator()(int n) { sum += n; } }; Sum s = std::for_each(v.begin(), v.end(), Sum());
Véase también lambdas.
[editar] Incremento y decremento
Cuando el incremento y decremento posfijos aparecen en una expresión, se llama a las funciones correspondientes definidas por el usuario (operator++ u operator--) con un argumento entero 0
. Típicamente, se implementa como T operator++(int), donde se ignora el argumento. Los operadores de incremento y decremento posfijo habitualmente se implementan en términos de la versión prefija:
struct X { X& operator++() { // el incremento actual toma lugar aquí return *this; } X operator++(int) { X tmp(*this); // copia operator++(); // preincremento return tmp; // devuelve valor viejo } };
Aunque la forma canónica de preincremento/predecremento devuelve una referencia, similar a cualquier sobrecarga de un operador, el tipo de retorno está definido por el usuario. Por ejemplo, las sobrecargas de estos operadores para std::atomic devuelven por valor.
[editar] Operadores aritméticos binarios
Los operadores binarios se implementan típicamente como no miembros para mantener simetría (por ejemplo, al agregar un número complejo y un entero, si operator+
es una función miembro del tipo complejo, entonces solamente complex+integer compilaría, y no integer+complex). Ya que para cada operador aritmético binario existe un operador compuesto de asignación correspondiente, las formas canónicas de los operadores binarios se implementan en términos de sus asignaciones compuestas:
class X { public: X& operator+=(const X& rhs) // asignación compuesta (no necesita ser miembro, { // pero frecuentemente lo es para modificar // los miembros privados) /* adición de rhs a *this toma lugar aquí */ return *this; // devuelve el resultado por referencia } // funciones friend definidas dentro del cuerpo de la función // son en línea y se ocultan de la búsqueda no ADL // (búsqueda dependiente de argumento) friend X operator+(X lhs, // pasar lhs por valor ayuda a optimizar la cadena a+b+c const X& rhs) // de lo contrario, ambos parámetros pueden ser // referencias const { lhs += rhs; // reutilizar asignación compuesta return lhs; // devolver el resultado por valor (utiliza constructor de movimiento) } };
[editar] Operadores relacionales
Los algoritmos como std::sort y los contenedores como std::set esperan que el operador operator< se defina, por defecto, para tipos proporcionados por el usuario, y se espera que implemente un ordenamiento débil estricto (de esta manera satisfacen los requisitos de Compare). Una manera idiomática de implementar el ordenamiento débil estricto para una estructura es usar la comparación lexicográfica proporcionada por std::tie:
struct Registro { std::string nombre; unsigned int piso; double peso; friend bool operator<(const Registro& l, const Registro& r) { return std::tie(l.nombre, l.piso, l.peso) < std::tie(r.nombre, r.piso, r.peso); // mantiene el mismo orden } };
Típicamente, una vez que se proporciona el operador operator<, los otros operadores relacionales se implementan en términos del operador operator<.
inline bool operator< (const X& lhs, const X& rhs){ /* hace la comparación actual */ } inline bool operator> (const X& lhs, const X& rhs){ return rhs < lhs; } inline bool operator<=(const X& lhs, const X& rhs){ return !(lhs > rhs); } inline bool operator>=(const X& lhs, const X& rhs){ return !(lhs < rhs); }
De la misma manera, el operador de desigualdad se implementa típicamente en términos del operador operator==:
inline bool operator==(const X& lhs, const X& rhs){ /* hace la comparación actual */ } inline bool operator!=(const X& lhs, const X& rhs){ return !(lhs == rhs); }
Cuando se proporciona la comparación de tres vías (tal como std::memcmp o std::string::compare), todos los seis operadores relacionales pueden expresarse mediante eso:
inline bool operator==(const X& lhs, const X& rhs){ return cmp(lhs,rhs) == 0; } inline bool operator!=(const X& lhs, const X& rhs){ return cmp(lhs,rhs) != 0; } inline bool operator< (const X& lhs, const X& rhs){ return cmp(lhs,rhs) < 0; } inline bool operator> (const X& lhs, const X& rhs){ return cmp(lhs,rhs) > 0; } inline bool operator<=(const X& lhs, const X& rhs){ return cmp(lhs,rhs) <= 0; } inline bool operator>=(const X& lhs, const X& rhs){ return cmp(lhs,rhs) >= 0; }
El comparador de desigualdad se genera automáticamente por el compilador si se define el operador operator==. De la misma manera, los cuatro operadores relacionales se generan automáticamente por el compilador si se define el operador de comparación de tres vías operator<=>. Los operadores operator== y operator<=>, a su vez, se generan por el compilador si el operador operator<=> se define como por defecto: struct Registro { std::string nombre; unsigned int piso; double peso; auto operator<=>(const Registro&) = default; // por defecto }; // ahora los registros pueden compararse con ==, !=, <, <=, >, y >= Véase comparaciones por defecto para más detalles. |
(desde C++20) |
[editar] Operador de subíndice de array
Las clases definidas por el usuario que proveen acceso similar a un array que permite tanto la lectura como la escritura, típicamente definen dos sobrecargas para el operador operator[]: variantes const
y no const
:
struct T { value_t& operator[](std::size_t idx) { return mVector[idx]; } const value_t& operator[](std::size_t idx) const { return mVector[idx]; } };
Si se sabe que el tipo de valor es un tipo integrado, la variante const
deberá devolver por valor.
Donde no se requiere acceso directo a los elementos del contenedor o no es posible distinguir entre el uso de lvalue c[i] = v; y rvalue v = c[i];, operator[] puede devolver un representante (proxy). Véase por ejemplo a std::bitset::operator[].
Para proporcionar la semántica de acceso de arrays multidimensionales, p. ej., para implementar acceso a un array de 3D a[i][j][k] = x;, operator[] debe devolver una referencia a un plano de 2D, que debe tener su propio operador operator[]], que devuelve una referencia a una fila de 1D, que debe tener un operador operator[], que devuelve una referencia al elemento. Para evitar esta complejidad, algunas bibliotecas optan por sobrecargar el operador operator() en su lugar, de tal manera que las expresiones de acceso 3D tengan una sintaxis similares a Fortran a(i, j, k) = x;
[editar] Operadores aritméticos bit a bit
Las clases definidas por el usuario y enumeraciones que implementan los requisitos de BitmaskType se requiere que sobrecarguen los operadores aritméticos bit a bit operator&, operator|, operator^, operator~, operator&=, operator|=, y operator^=, y opcionalmente pueden sobrecargar los operadores de desplazamiento operator<< operator>>, operator>>=, y operator<<=. Las implementaciones canónicas habitualmente siguen el patrón para los operadores aritméticos binarios descrito anteriormente.
[editar] Operador de negación Booleano
El operador operator! comúnmente se sobrecarga por las clases definidas por el usuario que pretenden utilizarse en contextos Booleanos. Tales clases también proporcionan una función de conversión definida por el usuario a un tipo Booleano (véase std::basic_ios para el ejemplo de la biblioteca estándar), y el comportamiento esperado de operator! es que devuelva el valor opuesto al de operator bool. |
(hasta C++11) |
Ya que el operador ! integrado realiza una conversión contextual a |
(desde C++11) |
[editar] Operadores raramente sobrecargados
Los siguientes operadores raramente se sobrecargan:
- El operador dirección-de, operator&. Si el & unario se aplica a un lvalue de un tipo incompleto y el tipo completo declara una sobrecarga del operador operator&, el comportamiento es indefinido (hasta C++11) no está especificado si el operador tiene el significado del operador integrado o se llama a la función del operador (desde C++11). Ya que este operador puede sobrecargarse, las bibliotecas genéricas utilizan std::addressof para obtener las direcciones de objetos de tipos definidos por el usuario. El mejor ejemplo conocido de un operador canónico operator& sobrecargado es la clase de Microsoft CComPtr. Un ejemplo de uso de este operador en un lenguaje de dominio específico embebido (ESDL por sus siglas en inglés) puede encontrarse en boost.spirit.
- Los operadores lógicos Booleanos, operator&& y operator||. A diferencia de las versiones integradas, las sobrecargas no pueden implementar la evaluación de cortocircuito. También a diferencia de las versiones integradas, no secuencian su operador izquierdo antes que el derecho. (hasta C++17) En la biblioteca estándar, estos operadores solamente se sobrecargan para std::valarray.
- El operador coma, operator,. A diferencia de la versión integrada, las sobrecargas no secuencian su operando izquierdo antes que el derecho. (hasta C++17) Ya que este operador puede sobrecargarse, las bibliotecas genéricas usan expresiones como a,void(),b en lugar de a,b para secuenciar la ejecución de expresiones de tipos definidos por el usuario. La biblioteca Boost utiliza el operador
operator,
en boost.assign, boost.spirit, y otras bibliotecas. La biblioteca de acceso a bases de datos SOCI también sobrecarga el operadoroperator,
. - El accesso a miembro a través del operador puntero a miembro operator->*. No existen desventajas específicas para sobrecargar este operador, pero raramente se usa en la práctica. Se sugirió que podría ser parte de la interfaz de puntero inteligente, y de hecho se usa en esa capacidad por actores en boost.phoenix. Es más común en lenguajes de dominio específico embebidos (ESDLs por sus siglas en inglés) tales como cpp.react.
[editar] Ejemplo
#include <iostream> class Fraccion { int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); } int n, d; public: Fraccion(int n, int d = 1) : n(n/gcd(n, d)), d(d/gcd(n, d)) { } int num() const { return n; } // numerador int den() const { return d; } // denominador Fraccion& operator*=(const Fraccion& rhs) { int new_n = n * rhs.n/gcd(n * rhs.n, d * rhs.d); d = d * rhs.d/gcd(n * rhs.n, d * rhs.d); n = new_n; return *this; } }; std::ostream& operator<<(std::ostream& out, const Fraccion& f) { return out << f.num() << '/' << f.den() ; } bool operator==(const Fraccion& lhs, const Fraccion& rhs) { return lhs.num() == rhs.num() && lhs.den() == rhs.den(); } bool operator!=(const Fraccion& lhs, const Fraccion& rhs) { return !(lhs == rhs); } Fraccion operator*(Fraccion lhs, const Fraccion& rhs) { return lhs *= rhs; } int main() { Fraccion f1(3, 8), f2(1, 2), f3(10, 2); std::cout << f1 << " * " << f2 << " = " << f1 * f2 << '\n' << f2 << " * " << f3 << " = " << f2 * f3 << '\n' << 2 << " * " << f1 << " = " << 2 * f1 << '\n'; }
Salida:
3/8 * 1/2 = 3/16 1/2 * 5/1 = 5/2 2 * 3/8 = 3/4
[editar] Informes de defectos
Los siguientes informes de defectos de cambio de comportamiento se aplicaron de manera retroactiva a los estándares de C++ publicados anteriormente.
ID | Aplicado a | Comportamiento según lo publicado | Comportamiento correcto |
---|---|---|---|
CWG 1458 | C++11 | Tomar la dirección de un tipo incompleto que sobrecarga al operador dirección-de tenía comportamiento indefinido |
el comportamiento está solamente inespecificado |
[editar] Véase también
Operadores comunes | ||||||
---|---|---|---|---|---|---|
Asignación | Incremento/decremento | Aritméticos | Lógicos | Comparación | Acceso a miembro | Otros |
a = b |
++a |
+a |
!a |
a == b |
a[b] |
a(...) |
Operadores especiales | ||||||
static_cast Convierte de un tipo a otro tipo relacionado |
[editar] Referencias
- ↑ Sobrecarga de operadores en StackOverflow C++ FAQ