Operadores comunes para sobrecargar
La mayor parte del trabajo en operadores de sobrecarga es código de placa de caldera. No es de extrañar, ya que los operadores son simplemente azúcar sintáctica, su trabajo real podría realizarse (y a menudo se reenvía a) funciones simples. Pero es importante que obtenga este código correcto de placa de caldera. Si falla, el código de su operador no se compilará o el código de sus usuarios no se compilará o el código de sus usuarios se comportará sorprendentemente.
Operador de asignación
Hay mucho que decir sobre la asignación. Sin embargo, la mayor parte ya se ha dicho en las famosas preguntas frecuentes de Copiar e intercambiar de GMan, por lo que omitiré la mayor parte aquí, solo enumerando el operador de asignación perfecto para referencia:
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
Operadores Bitshift (utilizados para Stream I / O)
Los operadores de desplazamiento de bits <<
y >>
, aunque todavía se usan en la interfaz de hardware para las funciones de manipulación de bits que heredan de C, se han vuelto más frecuentes como operadores de entrada y salida de flujo sobrecargados en la mayoría de las aplicaciones. Para la sobrecarga de la guía como operadores de manipulación de bits, consulte la siguiente sección sobre Operadores aritméticos binarios. Para implementar su propio formato personalizado y lógica de análisis cuando su objeto se utiliza con iostreams, continúe.
Los operadores de flujo, entre los operadores sobrecargados más comúnmente, son operadores infix binarios para los cuales la sintaxis no especifica ninguna restricción sobre si deben ser miembros o no miembros. Dado que cambian su argumento izquierdo (alteran el estado de la secuencia), deberían, de acuerdo con las reglas generales, implementarse como miembros del tipo de operando izquierdo. Sin embargo, sus operandos izquierdos son flujos de la biblioteca estándar, y aunque la mayoría de los operadores de salida y entrada de flujo definidos por la biblioteca estándar se definen como miembros de las clases de flujo, cuando implementa operaciones de salida y entrada para sus propios tipos, no puede cambiar los tipos de flujo de la biblioteca estándar. Es por eso que necesita implementar estos operadores para sus propios tipos como funciones que no son miembros. Las formas canónicas de los dos son estas:
std::ostream& operator<<(std::ostream& os, const T& obj)
{
// write obj to stream
return os;
}
std::istream& operator>>(std::istream& is, T& obj)
{
// read obj from stream
if( /* no valid object of T found in stream */ )
is.setstate(std::ios::failbit);
return is;
}
Al implementar operator>>
, configurar manualmente el estado de la secuencia solo es necesario cuando la lectura en sí misma tuvo éxito, pero el resultado no es lo que se esperaría.
Operador de llamada de función
El operador de llamada a función, utilizado para crear objetos de función, también conocidos como functores, debe definirse como una función miembro , por lo que siempre tiene el this
argumento implícito de las funciones miembro. Aparte de esto, se puede sobrecargar para tomar cualquier número de argumentos adicionales, incluido cero.
Aquí hay un ejemplo de la sintaxis:
class foo {
public:
// Overloaded call operator
int operator()(const std::string& y) {
// ...
}
};
Uso:
foo f;
int a = f("hello");
En toda la biblioteca estándar de C ++, los objetos de función siempre se copian. Por lo tanto, sus propios objetos de función deberían ser baratos de copiar. Si un objeto de función necesita usar datos que son caros de copiar, es mejor almacenar esos datos en otro lugar y hacer que el objeto de función se refiera a ellos.
Operadores de comparación
Los operadores de comparación de infijo binario deben, de acuerdo con las reglas generales, implementarse como funciones no miembros 1 . La negación del prefijo unario !
debe (de acuerdo con las mismas reglas) implementarse como una función miembro. (pero generalmente no es una buena idea sobrecargarlo).
Los algoritmos std::sort()
y tipos de la biblioteca estándar (por ejemplo std::map
) siempre esperarán operator<
estar presentes. Sin embargo, los usuarios de su tipo también esperarán que todos los demás operadores estén presentes , así que si define operator<
, asegúrese de seguir la tercera regla fundamental de sobrecarga de operadores y también definir todos los demás operadores de comparación booleanos. La forma canónica de implementarlos es esta:
inline bool operator==(const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator!=(const X& lhs, const X& rhs){return !operator==(lhs,rhs);}
inline bool operator< (const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator> (const X& lhs, const X& rhs){return operator< (rhs,lhs);}
inline bool operator<=(const X& lhs, const X& rhs){return !operator> (lhs,rhs);}
inline bool operator>=(const X& lhs, const X& rhs){return !operator< (lhs,rhs);}
Lo importante a tener en cuenta aquí es que solo dos de estos operadores realmente hacen algo, los otros simplemente envían sus argumentos a cualquiera de estos dos para hacer el trabajo real.
La sintaxis para sobrecargar los operadores booleanos binarios restantes ( ||
, &&
) sigue las reglas de los operadores de comparación. Sin embargo, es muy poco probable que encuentre un caso de uso razonable para estos 2 .
1 Como con todas las reglas generales, a veces puede haber razones para romper esta también. Si es así, no olvide que el operando de la izquierda de los operadores de comparación binarios, que será para las funciones miembro *this
, también debe serlo const
. Por lo tanto, un operador de comparación implementado como una función miembro debería tener esta firma:
bool operator<(const X& rhs) const { /* do actual comparison with *this */ }
(Tenga const
en cuenta el al final.)
2 Cabe señalar que la versión incorporada de la semántica de acceso directo ||
y su &&
uso. Mientras que los definidos por el usuario (porque son azúcar sintáctica para las llamadas a métodos) no use la semántica de atajos. El usuario esperará que estos operadores tengan semántica de acceso directo, y su código puede depender de ello, por lo tanto, se recomienda NUNCA definirlos.
Operadores aritméticos
Operadores aritméticos unarios
Los operadores de incremento y decremento unarios vienen en sabor de prefijo y postfijo. Para distinguir uno del otro, las variantes de postfix toman un argumento int ficticio adicional. Si sobrecarga el incremento o decremento, asegúrese de implementar siempre las versiones de prefijo y postfix. Aquí está la implementación canónica de incremento, decremento sigue las mismas reglas:
class X {
X& operator++()
{
// do actual increment
return *this;
}
X operator++(int)
{
X tmp(*this);
operator++();
return tmp;
}
};
Tenga en cuenta que la variante postfix se implementa en términos de prefijo. También tenga en cuenta que postfix hace una copia adicional. 2
La sobrecarga de unario menos y más no es muy común y probablemente sea mejor evitarla. Si es necesario, probablemente deberían sobrecargarse como funciones miembro.
2 También tenga en cuenta que la variante de postfix hace más trabajo y, por lo tanto, es menos eficiente de usar que la variante de prefijo. Esta es una buena razón para preferir generalmente el incremento de prefijo sobre el incremento de postfix. Si bien los compiladores generalmente pueden optimizar el trabajo adicional del incremento de postfix para los tipos incorporados, es posible que no puedan hacer lo mismo para los tipos definidos por el usuario (que podría ser algo tan inocentemente como un iterador de lista). Una vez que se haya acostumbrado a hacerlo i++
, se hace muy difícil recordar hacerlo ++i
cuando i
no es de tipo incorporado (además, tendría que cambiar el código al cambiar un tipo), por lo que es mejor acostumbrarse siempre usando el incremento de prefijo, a menos que se necesite explícitamente postfix.
Operadores aritméticos binarios
Para los operadores aritméticos binarios, no olvide obedecer la tercera sobrecarga del operador de la regla básica: si proporciona +
, también proporciona +=
, si proporciona -
, no omita -=
, etc. Se dice que Andrew Koenig fue el primero en observar que la asignación compuesta Los operadores se pueden utilizar como base para sus homólogos no compuestos. Es decir, el operador +
se implementa en términos de +=
, -
se implementa en términos de -=
etc.
De acuerdo con nuestras reglas generales, +
y sus compañeros deben ser no miembros, mientras que sus contrapartes de asignación compuesta ( +=
etc.), cambiando su argumento izquierdo, deben ser miembros. Aquí está el código ejemplar para +=
y +
; Los otros operadores aritméticos binarios deben implementarse de la misma manera:
class X {
X& operator+=(const X& rhs)
{
// actual addition of rhs to *this
return *this;
}
};
inline X operator+(X lhs, const X& rhs)
{
lhs += rhs;
return lhs;
}
operator+=
devuelve su resultado por referencia, mientras que operator+
devuelve una copia de su resultado. Por supuesto, devolver una referencia suele ser más eficiente que devolver una copia, pero en el caso de que operator+
no haya forma de evitar la copia. Cuando escribe a + b
, espera que el resultado sea un nuevo valor, por lo que operator+
debe devolver un nuevo valor. 3
También tenga en cuenta que operator+
toma su operando izquierdo por copia en lugar de por referencia constante. La razón de esto es la misma que la razón que da para operator=
tomar su argumento por copia.
Los operadores de manipulación de bits ~
&
|
^
<<
>>
deben implementarse de la misma manera que los operadores aritméticos. Sin embargo, (excepto por sobrecarga <<
y >>
por salida y entrada) hay muy pocos casos de uso razonable para sobrecargarlos.
3 Nuevamente, la lección que se puede extraer de esto es que a += b
, en general, es más eficiente que, a + b
y debería preferirse, si es posible.
Subscripting de matriz
El operador de subíndice de matriz es un operador binario que debe implementarse como miembro de la clase. Se utiliza para tipos similares a contenedores que permiten el acceso a sus elementos de datos mediante una clave. La forma canónica de proporcionar estos es esta:
class X {
value_type& operator[](index_type idx);
const value_type& operator[](index_type idx) const;
// ...
};
A menos que no desee que los usuarios de su clase puedan cambiar los elementos de datos devueltos por operator[]
(en cuyo caso puede omitir la variante no constante), siempre debe proporcionar ambas variantes del operador.
Si se sabe que value_type se refiere a un tipo incorporado, la variante const del operador debería devolver mejor una copia en lugar de una referencia const:
class X {
value_type& operator[](index_type idx);
value_type operator[](index_type idx) const;
// ...
};
Operadores para tipos de puntero
Para definir sus propios iteradores o punteros inteligentes, debe sobrecargar el operador de desreferencia de prefijo unario *
y el operador de acceso de miembro de puntero infijo binario ->
:
class my_ptr {
value_type& operator*();
const value_type& operator*() const;
value_type* operator->();
const value_type* operator->() const;
};
Tenga en cuenta que estos también necesitarán casi siempre una versión const y una versión sin const. Para el ->
operador, si value_type
es de class
(o struct
o union
tipo), otro operator->()
se llama de forma recursiva, hasta que un operator->()
devuelve un valor de tipo no clase.
La dirección unaria del operador nunca debe sobrecargarse.
Para operator->*()
ver esta pregunta . Raramente se usa y, por lo tanto, rara vez se sobrecarga. De hecho, incluso los iteradores no lo sobrecargan.
Continuar a los operadores de conversión