Nota: Las respuestas fueron dadas en un orden específico, pero como muchos usuarios clasifican las respuestas según los votos, en lugar de la hora en que fueron dadas, aquí' hay un índice de las respuestas en el orden en que tienen más sentido:
sub> (Nota: Esta es una entrada para Stack Overflow's C++ FAQ. Si quieres criticar la idea de proporcionar un FAQ en esta forma, entonces la publicación en meta que comenzó todo esto sería el lugar para hacerlo. Las respuestas a esa pregunta se supervisan en la sala de chat de C++, donde surgió la idea de las FAQ en primer lugar, así que es muy probable que tu respuesta sea leída por aquellos a los que se les ocurrió la idea).
La mayor parte del trabajo en la sobrecarga de operadores es código de caldera. No es de extrañar, ya que los operadores son simplemente azúcar sintáctico, su trabajo real podría ser realizado por (y a menudo es reenviado a) funciones simples. Pero es importante que el código sea correcto. Si fallas, el código de tu operador no compilará o el código de tus usuarios no compilará o el código de tus usuarios se comportará de forma sorprendente.
Hay mucho que decir sobre la asignación. Sin embargo, la mayor parte ya se ha dicho en GMan's famous Copy-And-Swap FAQ, así que me saltaré la mayor parte aquí, sólo listando el operador de asignación perfecto como referencia:
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
Los operadores de desplazamiento de bits <<
y >>
, aunque todavía se utilizan en la interfaz de hardware para las funciones de manipulación de bits que heredan de C, se han convertido en más frecuentes como operadores de entrada y salida de flujo sobrecargados en la mayoría de las aplicaciones. Para obtener orientación sobre la sobrecarga como operadores de manipulación de bits, consulte la sección siguiente sobre Operadores Aritméticos Binarios. Para implementar su propio formato personalizado y la lógica de análisis cuando su objeto se utiliza con iostreams, continúe.
Los operadores de flujo, entre los operadores más comúnmente sobrecargados, son operadores binarios infijos para los que la sintaxis no especifica ninguna restricción sobre si deben ser miembros o no miembros.
Dado que cambian su argumento izquierdo (alteran el estado del flujo), deberían, según las reglas generales, implementarse como miembros del tipo de su 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 flujos definidos por la biblioteca estándar están efectivamente definidos como miembros de las clases de flujos, cuando implementas operaciones de salida y entrada para tus propios tipos, no puedes cambiar los tipos de flujos de la biblioteca estándar. Por eso necesitas implementar estos operadores para tus propios tipos como funciones no miembros.
Las formas canónicas de ambos 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;
}
Cuando se implementa el operador>>
, sólo es necesario establecer manualmente el estado del flujo cuando la lectura en sí tuvo éxito, pero el resultado no es el esperado.
El operador de llamada a función, utilizado para crear objetos de función, también conocidos como functores, debe ser definido como una función member, por lo que siempre tiene el argumento implícito this
de las funciones miembro. Aparte de esto, puede sobrecargarse para tomar cualquier número de argumentos adicionales, incluyendo cero.
He aquí 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 deben ser baratos de copiar. Si un objeto de función necesita absolutamente usar datos que son caros de copiar, es mejor almacenar esos datos en otro lugar y hacer que el objeto de función haga referencia a ellos.
Los operadores binarios de comparación infija deberían, según las reglas generales, implementarse como funciones no miembros1. La negación unaria del prefijo !
debería (según las mismas reglas) implementarse como una función miembro. (pero no suele ser buena idea sobrecargarla).
Los algoritmos de la biblioteca estándar (por ejemplo, std::sort()
) y los tipos (por ejemplo, std::map
) siempre esperarán que esté presente operator<
. Sin embargo, los usuarios de su tipo esperarán que todos los demás operadores estén presentes, así que si define operador<
, asegúrese de seguir la tercera regla fundamental de la sobrecarga de operadores y defina también todos los demás operadores booleanos de comparación. 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 es que sólo dos de estos operadores hacen realmente algo, los otros sólo envían sus argumentos a cualquiera de estos dos para que hagan el trabajo real.
La sintaxis para sobrecargar los restantes operadores booleanos binarios (||
, &&
) sigue las reglas de los operadores de comparación. Sin embargo, es muy improbable que encuentre un caso de uso razonable para estos2.
1 Como con todas las reglas generales, a veces puede haber razones para romper esta también. Si es así, no olvides que el operando izquierdo de los operadores de comparación binarios, que para las funciones miembro será *esto
, necesita ser const
también. Así que un operador de comparación implementado como función miembro tendría que tener esta firma:
bool operator<(const X& rhs) const { /* do actual comparison with *this */ }
(Nótese el const
al final.)
2 Hay que tener en cuenta que la versión incorporada de ||
y &&
utilizan la semántica de los atajos. Mientras que los definidos por el usuario (porque son azúcar sintáctico para las llamadas a métodos) no utilizan la semántica de atajo. El usuario esperará que estos operadores tengan semántica de atajo, y su código puede depender de ello, por lo que es altamente recomendable NUNCA definirlos.
Los operadores unarios de incremento y decremento están disponibles en versión prefija y postfija. Para distinguir uno de otro, las variantes postfijas toman un argumento int ficticio adicional. Si sobrecargas increment o decrement, asegúrate de implementar siempre las versiones prefijas y postfijas. Esta es la implementación canónica de incremento, el 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 note que postfix hace una copia extra.2
La sobrecarga de menos y más unarios no es muy común y probablemente es mejor evitarla. Si es necesario, probablemente deberían sobrecargarse como funciones miembro.
2 También hay que tener en cuenta que la variante postfija hace más trabajo y por lo tanto es menos eficiente de usar que la variante prefija. Esta es una buena razón para preferir generalmente el incremento del prefijo sobre el incremento del postfijo. Mientras que los compiladores normalmente pueden optimizar el trabajo adicional del incremento postfijo para los tipos incorporados, podrían no ser capaces de hacer lo mismo para los tipos definidos por el usuario (que podrían ser algo tan inocente como un iterador de lista). Una vez que te acostumbras a hacer i++
, se hace muy difícil recordar hacer ++i
en su lugar cuando i
no es de un tipo incorporado (además tendrías que cambiar el código al cambiar un tipo), por lo que es mejor hacer un hábito de usar siempre el incremento de prefijo, a menos que el postfijo sea explícitamente necesario.
Para los operadores aritméticos binarios, no olvide obedecer la tercera regla básica de sobrecarga de operadores: Si proporciona +
, proporcione también +=
, si proporciona -
, no omita -=
, etc. Se dice que Andrew Koenig fue el primero en observar que los operadores de asignación compuestos pueden utilizarse 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.
Según nuestras reglas generales, +
y sus compañeros deben ser no miembros, mientras que sus homólogos de asignación compuesta (+=
etc.), cambiando su argumento izquierdo, deben ser miembros. Aquí está el código de ejemplo para +=
y +
; los otros operadores aritméticos binarios deberían 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;
}
El operador+=
devuelve su resultado por referencia, mientras que el operador+
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 operador+
, no hay forma de evitar la copia. Cuando escribes a + b
, esperas que el resultado sea un nuevo valor, por lo que operador+
tiene que devolver un nuevo valor.3
También hay que tener en cuenta que operador+
toma su operando izquierdo por copia en lugar de por referencia constante. La razón de esto es la misma que la que se da para que operador=
tome su argumento por copia.
Los operadores de manipulación de bits ~
&
|
^
<<
>>
deberían implementarse de la misma manera que los operadores aritméticos. Sin embargo, (excepto para sobrecargar <<
y >>
para la salida y la entrada) hay muy pocos casos de uso razonables para sobrecargarlos.
3 De nuevo, la lección que debe extraerse de esto es que a += b
es, en general, más eficiente que a + b
y debe preferirse si es posible.
El operador de subíndice de matrices es un operador binario que debe ser implementado como un miembro de la clase. Se utiliza para tipos tipo contenedor que permiten el acceso a sus elementos de datos mediante una clave. La forma canónica de proporcionarlos es esta
class X {
value_type& operator[](index_type idx);
const value_type& operator[](index_type idx) const;
// ...
};
A menos que no quiera que los usuarios de su clase puedan cambiar los elementos de datos devueltos por operador[]
(en cuyo caso puede omitir la variante no-const), siempre debe proporcionar ambas variantes del operador.
Si se sabe que value_type se refiere a un tipo incorporado, es mejor que la variante const del operador devuelva una copia en lugar de una referencia const:
class X {
value_type& operator[](index_type idx);
value_type operator[](index_type idx) const;
// ...
};
Para definir tus propios iteradores o punteros inteligentes, tienes que sobrecargar el operador de desreferencia de prefijo unario *
y el operador de acceso a miembro de puntero infijo binario ->
:
class my_ptr {
value_type& operator*();
const value_type& operator*() const;
value_type* operator->();
const value_type* operator->() const;
};
->
, si tipo_valor
es de tipo clase
(o estructura
o unión
), se llama recursivamente a otro operador->()
, hasta que un operador->()
devuelva un valor de tipo no-clase.
El operador unario de dirección-de nunca debe ser sobrecargado.
Para operador->*()
vea esta pregunta. Es raramente usado y por lo tanto raramente sobrecargado. De hecho, ni siquiera los iteradores lo sobrecargan.Continuar con Operadores de conversión
Cuando se trata de la sobrecarga de operadores en C++, hay tres reglas básicas que debes seguir. Como con todas esas reglas, hay ciertamente excepciones. A veces la gente se ha desviado de ellas y el resultado no ha sido un mal código, pero tales desviaciones positivas son pocas y distantes. Como mínimo, 99 de cada 100 desviaciones de este tipo que he visto eran injustificadas. Sin embargo, también podrían haber sido 999 de cada 1000. Así que será mejor que te ciñas a las siguientes reglas.
Siempre que el significado de un operador no sea obviamente claro e indiscutible, no debe sobrecargarse. _En su lugar, proporcione una función con un nombre bien escogido.__. Básicamente, la primera y más importante regla para la sobrecarga de operadores, en su corazón, dice: No lo hagas. Esto puede parecer extraño, porque hay mucho que saber sobre la sobrecarga de operadores y por eso muchos artículos, capítulos de libros y otros textos tratan de todo esto. Pero a pesar de esta evidencia aparentemente obvia, sólo hay unos pocos casos sorprendentes en los que la sobrecarga de operadores es apropiada. La razón es que, en realidad, es difícil entender la semántica detrás de la aplicación de un operador a menos que el uso del operador en el dominio de la aplicación sea bien conocido e indiscutible. En contra de la creencia popular, esto casi nunca es así.
Siempre se debe respetar la semántica conocida del operador.
C++ no impone limitaciones a la semántica de los operadores sobrecargados. Su compilador aceptará felizmente código que implemente el operador binario +
para restar de su operando derecho. Sin embargo, los usuarios de dicho operador nunca sospecharían que la expresión a + b
resta a
de b
. Por supuesto, esto supone que la semántica del operador en el dominio de la aplicación es indiscutible.
__Siempre se proporciona todo de un conjunto de operaciones relacionadas.
Los operadores están relacionados entre sí y con otras operaciones. Si su tipo soporta a + b
, los usuarios esperarán poder llamar también a a += b
. Si soporta el incremento del prefijo ++a
, esperarán que a++
también funcione. Si pueden comprobar si a < b
, seguramente esperarán también poder comprobar si a > b
. Si pueden copiar-construir su tipo, esperan que la asignación también funcione.
Continúe con La decisión entre miembro y no miembro.
No se puede cambiar el significado de los operadores para los tipos incorporados en C++, los operadores sólo pueden ser sobrecargados para los tipos definidos por el usuario1. Es decir, al menos uno de los operandos tiene que ser de un tipo definido por el usuario. Al igual que con otras funciones sobrecargadas, los operadores pueden ser sobrecargados para un determinado conjunto de parámetros sólo una vez.
No todos los operadores pueden sobrecargarse en C++. Entre los operadores que no se pueden sobrecargar están: .
::
sizeof
typeid
.*
y el único operador ternario en C++, ?:
Entre los operadores que se pueden sobrecargar en C++ están estos:
+
-
*
/
%
y +=
-=
*=
/=
%=
(todos infijos binarios); +
-
(prefijo unario); ++
--
(prefijo y postfijo unario)&
|
^
<<
>>
y &=
|=
^=
<<=
>=
(todos los infijos binarios); ~
(prefijo unario)==
!=
<
>
<=
||
&&
(todos los infijos binarios); !
(prefijo unario)new
new[]
delete
delete[]
=
[]
->`->*
,
(todos los infijos binarios); *
&
(todos los prefijos unarios) ()
(llamada a función, infijo n-ario)Sin embargo, el hecho de que pueda sobrecargar todos estos operadores no significa que deba hacerlo. Vea las reglas básicas de la sobrecarga de operadores.
En C++, los operadores se sobrecargan en forma de funciones con nombres especiales. Al igual que con otras funciones, los operadores sobrecargados generalmente pueden ser implementados como una función miembro del tipo de su operando izquierdo o como funciones no miembros. La libertad de elección o la obligación de utilizar una u otra depende de varios criterios.2 Un operador unario @
3, aplicado a un objeto x, se invoca como operador@(x)
o como x.operador@()
. Un operador infijo binario @
, aplicado a los objetos x
y y
, se invoca como operator@(x,y)
o como x.operator@(y)
.4
Los operadores que se implementan como funciones no miembros son a veces amigos del tipo de su operando.
1 El término "definido por el usuario" puede ser ligeramente engañoso. C++ distingue entre tipos incorporados y tipos definidos por el usuario. A los primeros pertenecen, por ejemplo, int, char y double; a los segundos, todos los tipos struct, class, union y enum, incluidos los de la biblioteca estándar, aunque no estén, como tales, definidos por los usuarios.
2 Esto se trata en una parte posterior de este FAQ.
3 El @
no es un operador válido en C++ por lo que lo uso como marcador de posición.
4 El único operador ternario en C++ no puede ser sobrecargado y el único operador n-ario debe ser implementado siempre como una función miembro.
Continúe con Las tres reglas básicas de la sobrecarga de operadores en C++.