operator overloading
Personaliza los operadores de C++ para operandos de tipos definidos por el usuario.
Sintaxis
Funciones de operador son funciones con nombres de función especiales:
operator
op
|
(1) | ||||||||
operator
new
operator
new []
|
(2) | ||||||||
operator
delete
operator
delete []
|
(3) | ||||||||
operator
co_await
|
(4) | (desde C++20) | |||||||
| op | - | cualquiera de los siguientes operadores: + - * / % ^ & | ~ ! = < > + = - = * = / = % = ^ = & = | = << >> >>= <<= == ! = <= >= <=> (desde C++20) && || ++ -- , - > * - > ( ) [ ] |
Los comportamientos de los operadores no de puntuación se describen en sus respectivas páginas. A menos que se especifique lo contrario, la descripción restante en esta página no se aplica a estas funciones.
Explicación
Cuando un operador aparece en una expresión , y al menos uno de sus operandos tiene un class type o un enumeration type , entonces se utiliza overload resolution para determinar la función definida por el usuario que será llamada entre todas las funciones cuyas firmas 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 operator == o operator <=> . |
(desde C++20) |
Los operadores sobrecargados (pero no los operadores incorporados) pueden llamarse usando notación de función:
std::string str = "Hola, "; str.operator+=("mundo"); // igual que str += "mundo"; operator<<(operator<<(std::cout, str), '\n'); // igual que std::cout << str << '\n'; // (desde C++17) excepto por secuenciación
Operadores sobrecargados estáticosLos operadores sobrecargados que son funciones miembro pueden declararse static . Sin embargo, esto solo está permitido para operator ( ) y operator [ ] . Dichos operadores pueden invocarse utilizando notación de función. Sin embargo, cuando estos operadores aparecen en expresiones, aún requieren un objeto de tipo clase. struct SwapThem { template<typename T> static void operator()(T& lhs, T& rhs) { std::ranges::swap(lhs, rhs); } template<typename T> static void operator[](T& lhs, T& rhs) { std::ranges::swap(lhs, rhs); } }; inline constexpr SwapThem swap_them{}; void foo() { int a = 1, b = 2; swap_them(a, b); // OK swap_them[a, b]; // OK SwapThem{}(a, b); // OK SwapThem{}[a, b]; // OK SwapThem::operator()(a, b); // OK SwapThem::operator[](a, b); // OK SwapThem(a, b); // error, invalid construction SwapThem[a, b]; // error } |
(desde C++23) |
Restricciones
- Una función de operador debe tener al menos un parámetro de función o parámetro de objeto implícito cuyo tipo sea una clase, una referencia a una clase, una enumeración o una referencia a una enumeración.
-
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 como
**,<>, o&|. - No es posible cambiar la precedencia, agrupación o número de operandos de los operadores.
-
La sobrecarga del operador
->debe retornar un puntero crudo, o retornar un objeto (por referencia o por valor) para el cual el operador->esté a su vez sobrecargado. -
Las sobrecargas de los operadores
&&y||pierden la evaluación de cortocircuito.
|
(hasta C++17) |
Implementaciones canónicas
Además de las restricciones anteriores, el lenguaje no impone otras limitaciones 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 a los operadores integrados: operator + se espera que sume, en lugar de multiplicar sus argumentos, operator = se espera que asigne, etc. Se espera que los operadores relacionados se comporten de manera similar ( operator + y operator + = realizan la misma operación de tipo suma). 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 retornan por referencia para hacer posible escribir a = b = c = d , porque los operadores integrados permiten eso.
Los operadores comúnmente sobrecargados tienen las siguientes formas típicas y canónicas: [1]
Operador de asignación
El operador de asignación operator = tiene propiedades especiales: consulte asignación de copia y asignación de movimiento para más detalles.
Se espera que el operador de asignación por copia canónico sea seguro ante autoasignación , y que retorne la referencia al lhs:
// asignación de copia T& operator=(const T& other) { // Proteger autoasignación if (this == &other) return *this; // asumir que *this gestiona un recurso reutilizable, como un búfer asignado en el montón mArray if (size != other.size) // el recurso en *this no puede reutilizarse { temp = new int[other.size]; // asignar recurso, si lanza excepción, no hacer nada delete[] mArray; // liberar recurso en *this mArray = temp; 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 fuente en un estado válido (es decir, un estado con las invariantes de clase intactas), y que no haga nada o al menos deje el objeto en un estado válido en caso de auto-asignación, y que retorne el lhs por referencia a no-const, y sea noexcept: // move assignment T& operator=(T&& other) noexcept { // Guard self assignment if (this == &other) return *this; // delete[]/size=0 would also be ok delete[] mArray; // release resource in *this mArray = std::exchange(other.mArray, nullptr); // leave other in valid state size = std::exchange(other.size, 0); return *this; } |
(desde C++11) |
En aquellas situaciones donde la asignación por copia no puede beneficiarse de la reutilización de recursos (no gestiona un array asignado en el heap y no tiene un miembro (posiblemente transitivo) que lo haga, como un miembro std::vector o std::string ), existe una abreviación conveniente popular: el operador de asignación de copia e intercambio (copy-and-swap), que toma su parámetro por valor (funcionando así tanto para asignación por copia como por movimiento dependiendo de la categoría de valor del argumento), intercambia con el parámetro, y deja que el destructor lo limpie.
// asignación de copia (idioma copy-and-swap) T& T::operator=(T other) noexcept // llama al constructor de copia o movimiento para construir other { std::swap(size, other.size); // intercambia recursos entre *this y other std::swap(mArray, other.mArray); return *this; } // se llama al destructor de other para liberar los recursos gestionados anteriormente por *this
Este formulario proporciona automáticamente strong exception guarantee , pero prohíbe la reutilización de recursos.
Extracción e inserción de flujos
Las sobrecargas de
operator>>
y
operator<<
que toman un
std::
istream
&
o
std::
ostream
&
como argumento izquierdo se conocen como operadores de inserción y extracción. Dado que toman el tipo definido por el usuario como argumento derecho (
b
en
a @ b
), deben implementarse como no miembros.
std::ostream& operator<<(std::ostream& os, const T& obj) { // escribir obj al stream return os; } std::istream& operator>>(std::istream& is, T& obj) { // leer obj desde stream if (/* T no pudo ser construido */) is.setstate(std::ios::failbit); return is; }
Estos operadores a veces se implementan como friend functions .
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 este tipo puede utilizarse en una expresión de llamada 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); }
Muchos algoritmos de la biblioteca estándar aceptan FunctionObject s para personalizar el comportamiento. No existen formas canónicas particularmente notables de operator ( ) , pero para ilustrar el uso:
#include <algorithm> #include <iostream> #include <vector> struct Sum { int sum = 0; void operator()(int n) { sum += n; } }; int main() { std::vector<int> v = {1, 2, 3, 4, 5}; Sum s = std::for_each(v.begin(), v.end(), Sum()); std::cout << "The sum is " << s.sum << '\n'; }
Salida:
The sum is 15
Incremento y decremento
Cuando el operador de incremento o decremento postfijo aparece en una expresión, se llama a la función definida por el usuario correspondiente ( operator ++ o operator -- ) con un argumento entero 0 . Normalmente, se declara como T operator ++ ( int ) o T operator -- ( int ) , donde el argumento se ignora. Los operadores de incremento y decremento postfijos generalmente se implementan en términos de las versiones prefijas:
struct X { // incremento prefijo X& operator++() { // el incremento real ocurre aquí return *this; // devolver nuevo valor por referencia } // incremento postfijo X operator++(int) { X old = *this; // copiar valor antiguo operator++(); // incremento prefijo return old; // devolver valor antiguo } // decremento prefijo X& operator--() { // el decremento real ocurre aquí return *this; // devolver nuevo valor por referencia } // decremento postfijo X operator--(int) { X old = *this; // copiar valor antiguo operator--(); // decremento prefijo return old; // devolver valor antiguo } };
Aunque las implementaciones canónicas de los operadores de incremento y decremento prefijos devuelven por referencia, como con cualquier sobrecarga de operador, el tipo de retorno es definido por el usuario; por ejemplo las sobrecargas de estos operadores para std::atomic devuelven por valor.
Operadores aritméticos binarios
Los operadores binarios normalmente se implementan como no-miembros para mantener la simetría (por ejemplo, al sumar un número complejo y un entero, si operator + es una función miembro del tipo complex, entonces solo complex + integer compilaría, y no integer + complex ). Dado que para cada operador aritmético binario existe un operador de asignación compuesta 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 a menudo lo es, para modificar los miembros privados) /* la adición de rhs a *this ocurre aquí */ return *this; // devuelve el resultado por referencia } // los amigos definidos dentro del cuerpo de la clase son inline y están ocultos de la búsqueda no-ADL friend X operator+(X lhs, // pasar lhs por valor ayuda a optimizar cadenas a+b+c const X& rhs) // de lo contrario, ambos parámetros pueden ser referencias constantes { lhs += rhs; // reutiliza la asignación compuesta return lhs; // devuelve el resultado por valor (usa el constructor de movimiento) } };
Operadores de comparación
Los algoritmos de la biblioteca estándar como std::sort y los contenedores como std::set esperan que operator < esté definido, por defecto, para los tipos proporcionados por el usuario, y esperan que implemente un ordenamiento débil estricto (satisfaciendo así los requisitos de Compare ). Una forma idiomática de implementar el ordenamiento débil estricto para una estructura es usar la comparación lexicográfica proporcionada por std::tie :
struct Record { std::string name; unsigned int floor; double weight; friend bool operator<(const Record& l, const Record& r) { return std::tie(l.name, l.floor, l.weight) < std::tie(r.name, r.floor, r.weight); // mantener el mismo orden } };
Normalmente, una vez que se proporciona operator < , los demás operadores relacionales se implementan en términos de operator < .
inline bool operator< (const X& lhs, const X& rhs) { /* realizar comparación real */ } 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); }
Del mismo modo, el operador de desigualdad normalmente se implementa en términos de operator == :
inline bool operator==(const X& lhs, const X& rhs) { /* realizar comparación real */ } inline bool operator!=(const X& lhs, const X& rhs) { return !(lhs == rhs); }
Cuando se proporciona una comparación de tres vías (como std::memcmp o std::string::compare ), todos los seis operadores de comparación bidireccionales pueden expresarse a través de ella:
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; }
Operador de subíndice de array
Las clases definidas por el usuario que proporcionan acceso similar a un array permitiendo tanto lectura como escritura normalmente definen dos sobrecargas para 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]; } };
|
Alternativamente, pueden expresarse como una única función miembro plantilla usando un parámetro de objeto explícito : struct T { decltype(auto) operator[](this auto& self, std::size_t idx) { return self.mVector[idx]; } }; |
(desde C++23) |
Si se sabe que el tipo de valor es un tipo escalar, la variante const debe retornar por valor.
Cuando no se desea o no es posible el acceso directo a los elementos del contenedor, o cuando es necesario distinguir entre uso lvalue c [ i ] = v ; y rvalue v = c [ i ] ; , operator [ ] puede devolver un proxy. Consulte por ejemplo std::bitset::operator[] .
|
operator [ ] solo puede tomar un subíndice. Para proporcionar semántica de acceso a arreglos multidimensionales, por ejemplo, para implementar un acceso a arreglo 3D a [ i ] [ j ] [ k ] = x ; , operator [ ] debe devolver una referencia a un plano 2D, que debe tener su propio operator [ ] que devuelve una referencia a una fila 1D, que debe tener operator [ ] que devuelve una referencia al elemento. Para evitar esta complejidad, algunas bibliotecas optan por sobrecargar operator ( ) en su lugar, de modo que las expresiones de acceso 3D tengan la sintaxis similar a Fortran a ( i, j, k ) = x ; . |
(hasta C++23) |
|
operator [ ] puede tomar cualquier número de subíndices. Por ejemplo, un operator [ ] de una clase de arreglo 3D declarado como T & operator [ ] ( std:: size_t x, std:: size_t y, std:: size_t z ) ; puede acceder directamente a los elementos.
Ejecutar este código
#include <array> #include <cassert> #include <iostream> template<typename T, std::size_t Z, std::size_t Y, std::size_t X> struct Array3d { std::array<T, X * Y * Z> m{}; constexpr T& operator[](std::size_t z, std::size_t y, std::size_t x) // C++23 { assert(x < X and y < Y and z < Z); return m[z * Y * X + y * X + x]; } }; int main() { Array3d<int, 4, 3, 2> v; v[3, 2, 1] = 42; std::cout << "v[3, 2, 1] = " << v[3, 2, 1] << '\n'; } Salida: v[3, 2, 1] = 42 |
(desde C++23) |
Operadores aritméticos bit a bit
Las clases y enumeraciones definidas por el usuario que implementan los requisitos de BitmaskType deben sobrecargar 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 generalmente siguen el patrón para operadores aritméticos binarios descrito anteriormente.
Operador de negación booleana
|
El operador operator ! comúnmente es sobrecargado por las clases definidas por el usuario que están destinadas a ser utilizadas en contextos booleanos. Dichas clases también proporcionan una función de conversión definida por el usuario al tipo booleano (ver std::basic_ios para el ejemplo de la biblioteca estándar), y el comportamiento esperado de operator ! es devolver el valor opuesto de operator bool . |
(hasta C++11) |
|
Dado que el operador incorporado ! realiza conversión contextual a bool , las clases definidas por el usuario que están destinadas a ser utilizadas en contextos booleanos podrían proporcionar únicamente operator bool y no necesitan sobrecargar operator ! . |
(desde C++11) |
Operadores raramente sobrecargados
Los siguientes operadores rara vez se sobrecargan:
-
El operador de dirección,
operator
&
. Si el operador unario & se aplica a un lvalue de tipo incompleto y el tipo completo declara un
operator
&
sobrecargado, no está especificado si el operador tiene el significado incorporado o se llama a la función operadora. Debido a que este operador puede sobrecargarse, las bibliotecas genéricas utilizan
std::addressof
para obtener direcciones de objetos de tipos definidos por el usuario. El ejemplo más conocido de un
operator
&
sobrecargado canónico es la clase de Microsoft
CComPtrBase. Un ejemplo del uso de este operador en EDSL se puede encontrar en boost.spirit . - Los operadores lógicos booleanos, operator && y operator || . A diferencia de las versiones incorporadas, las sobrecargas no pueden implementar evaluación de cortocircuito. También a diferencia de las versiones incorporadas, no secuencian su operando izquierdo antes del derecho. (hasta C++17) En la biblioteca estándar, estos operadores solo están sobrecargados para std::valarray .
- El operador coma, operator, . A diferencia de la versión incorporada, las sobrecargas no secuencian su operando izquierdo antes del derecho. (hasta C++17) Debido a que este operador puede sobrecargarse, las bibliotecas genéricas utilizan 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 operator, en boost.assign , boost.spirit , y otras bibliotecas. La biblioteca de acceso a bases de datos SOCI también sobrecarga operator, .
- El operador de acceso a miembro a través de puntero a miembro operator - > * . No hay desventajas específicas al sobrecargar este operador, pero rara vez se utiliza en la práctica. Se sugirió que podría ser parte de una interfaz de puntero inteligente , y de hecho se utiliza en esa capacidad por actores en boost.phoenix . Es más común en EDSLs como cpp.react .
Notas
| Macro de prueba de características | Valor | Estándar | Característica |
|---|---|---|---|
__cpp_static_call_operator
|
202207L
|
(C++23) | static operator ( ) |
__cpp_multidimensional_subscript
|
202211L
|
(C++23) | static operator [ ] |
Palabras clave
Ejemplo
#include <iostream> class Fraction { // or C++17's std::gcd constexpr int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); } int n, d; public: constexpr Fraction(int n, int d = 1) : n(n / gcd(n, d)), d(d / gcd(n, d)) {} constexpr int num() const { return n; } constexpr int den() const { return d; } constexpr Fraction& operator*=(const Fraction& 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 Fraction& f) { return out << f.num() << '/' << f.den(); } constexpr bool operator==(const Fraction& lhs, const Fraction& rhs) { return lhs.num() == rhs.num() && lhs.den() == rhs.den(); } constexpr bool operator!=(const Fraction& lhs, const Fraction& rhs) { return !(lhs == rhs); } constexpr Fraction operator*(Fraction lhs, const Fraction& rhs) { return lhs *= rhs; } int main() { constexpr Fraction 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'; static_assert(f3 == f2 * 10); }
Salida:
3/8 * 1/2 = 3/16 1/2 * 5/1 = 5/2 2 * 3/8 = 3/4
Informes de defectos
Los siguientes informes de defectos que modifican el comportamiento se aplicaron retroactivamente a los estándares publicados anteriormente de C++.
| DR | Aplicado a | Comportamiento publicado | Comportamiento correcto |
|---|---|---|---|
| CWG 1481 | C++98 |
el operador de incremento prefijo no miembro solo podía tener un parámetro
de tipo clase, tipo enumeración, o un tipo referencia a dichos tipos |
sin requisito de tipo |
| CWG 2931 | C++23 |
las funciones de operador miembro con objeto explícito solo podían tener ningún parámetro
de tipo clase, tipo enumeración, o un tipo referencia a dichos tipos |
prohibido |
Véase también
| Operadores comunes | ||||||
|---|---|---|---|---|---|---|
| asignación |
incremento
decremento |
aritméticos | lógicos | comparación |
acceso a
miembros |
otros |
|
a
=
b
|
++
a
|
+
a
|
!
a
|
a
==
b
|
a
[
...
]
|
llamada a función
a ( ... ) |
|
coma
a, b |
||||||
|
condicional
a ? b : c |
||||||
| Operadores especiales | ||||||
|
static_cast
convierte un tipo a otro tipo relacionado
|
||||||
Enlaces externos
|