¿Cuáles son las reglas básicas y los modismos para la sobrecarga de operadores?

Nota: Las respuestas se dieron en un pedido específico, pero dado que muchos usuarios clasifican las respuestas según los votos, en lugar de la hora a la que se les dio, aquí índice de las respuestas en el orden en que tengan más sentido:

(Nota: Esto está destinado a ser una entrada a Preguntas frecuentes sobre C ++ de Stack Overflow. Si desea criticar la idea de proporcionar preguntas frecuentes en este formulario, entonces la publicación en meta que comenzó todo esto sería el lugar para hacer eso. Las respuestas a esa pregunta se supervisan en el Sala de chat C ++, donde la idea de las preguntas frecuentes comenzó en primer lugar, por lo que es muy probable que quienes hayan ideado la idea lean su respuesta).

preguntado el 12 de diciembre de 10 a las 09:12

Si vamos a continuar con la etiqueta C ++ - FAQ, así es como se deben formatear las entradas. -

He escrito una breve serie de artículos para la comunidad alemana de C ++ sobre la sobrecarga de operadores: Parte 1: sobrecarga de operadores en C ++ cubre semántica, usos típicos y especialidades para todos los operadores. Tiene algunas coincidencias con sus respuestas aquí, sin embargo, hay información adicional. Las partes 2 y 3 hacen un tutorial para usar Boost.Operators. ¿Quieres que las traduzca y las agregue como respuestas? -

Ah, y una traducción al inglés también está disponible: los basicos y práctica común -

El operador de dirección operator& Está perdido. -

@ Red.Wave: En realidad, hay una oración, incluso en su propio párrafo, al final de la respuesta del operador común, pero dice "no hagas esto". Creo que fue Pete Becker de Dinkumware (la compañía que fabricó la librería estándar que luego compró Microsoft) quien dijo una vez que aquellos que sobrecargan operator&() y luego esperar que los tipos resultantes funcionen con la biblioteca estándar debería ser forzado a implementar una lib estándar que realice este milagro. IOW, si cree que tiene una aplicación para sobrecargar este operador, me gustaría saberlo. (Sin embargo, no contenga la respiración esperando un aplauso).

7 Respuestas

Operadores comunes para sobrecargar

La mayor parte del trabajo en los operadores de sobrecarga es el código de placa de caldera. Eso no es de extrañar, dado que los operadores son simplemente azúcar sintáctico, su trabajo real podría realizarse mediante (y a menudo se reenvía a) funciones simples. Pero es importante que obtenga este código de placa de caldera correctamente. 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á de manera sorprendente.

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 sobre copiar e intercambiar de GMan, así que omitiré la mayor parte aquí, solo enumeraré el operador de asignación perfecto como referencia:

X& X::operator=(X rhs)
{
  swap(rhs);
  return *this;
}

Operadores Bitshift (usados ​​para Stream I / O)

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 vuelto 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 lógica de análisis cuando su objeto se usa con iostreams, continúe.

Los operadores de flujo, entre los operadores sobrecargados más comúnmente, son operadores infijos 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 su operando izquierdo. Sin embargo, sus operandos izquierdos son flujos de la biblioteca estándar, y aunque la mayoría de los operadores de entrada y salida de flujo definidos por la biblioteca estándar se definen de hecho como miembros de las clases de flujo, cuando implementa operaciones de entrada y salida 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 no miembros. Las formas canónicas de los dos son las siguientes:

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í se realizó correctamente, pero el resultado no es el esperado.

Operador de llamada de función

El operador de llamada de función, que se utiliza para crear objetos de función, también conocidos como functores, debe definirse como un miembro función, por lo que siempre tiene la implícita this argumento de funciones miembro. Aparte de esto, se puede sobrecargar para aceptar cualquier número de argumentos adicionales, incluido cero.

A continuación, se muestra un ejemplo de 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 absolutamente usar datos que son costosos de copiar, es mejor almacenar esos datos en otro lugar y hacer que el objeto de función se refiera a él.

Operadores de comparación

Los operadores de comparación de infijos binarios deben, de acuerdo con las reglas generales, implementarse como funciones no miembros1. La negación del prefijo unario ! debería (de acuerdo con las mismas reglas) implementarse como una función miembro. (pero normalmente no es una buena idea sobrecargarlo).

Los algoritmos de la biblioteca estándar (p. Ej. std::sort()) y tipos (p. ej. std::map) siempre solo esperará operator< Ser presente. sin embargo, el los usuarios de su tipo esperarán que todos los demás operadores estén presentes, también, así que si defines operator<, asegúrese de seguir la tercera regla fundamental de sobrecarga de operadores y también defina 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 que hagan el trabajo real.

La sintaxis para sobrecargar los operadores booleanos binarios restantes (||, &&) sigue las reglas de los operadores de comparación. Sin embargo lo és Es poco probable que encuentre un caso de uso razonable para estos2.

1 Al igual que con todas las reglas generales, a veces también puede haber razones para romper esta. Si es así, no olvide que el operando de la izquierda de los operadores de comparación binaria, que para las funciones miembro será *this, necesita ser const, también. Entonces, 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 */ }

(Nota la const al final.)

2 Cabe señalar que la versión incorporada de || y && utilizar semántica de atajos. Mientras que los definidos por el usuario (porque son azúcar sintáctico para llamadas a métodos) no usan semántica de atajos. El usuario esperará que estos operadores tengan semántica de atajo, y su código puede depender de ello. Por lo tanto, se recomienda encarecidamente NUNCA definirlos.

Operadores aritméticos

Operadores aritméticos unarios

Los operadores unarios de incremento y decremento vienen tanto en forma de prefijo como de sufijo. Para distinguir una de la otra, las variantes de sufijo toman un argumento int ficticio adicional. Si sobrecarga el incremento o decremento, asegúrese de implementar siempre las versiones de prefijo y sufijo. 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 de sufijo se implementa en términos de prefijo. También tenga en cuenta que postfix hace una copia adicional.2

La sobrecarga unaria de 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 sufijo 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 el incremento de prefijo sobre el incremento de sufijo. Si bien los compiladores generalmente pueden optimizar el trabajo adicional del incremento de postfijo para los tipos integrados, es posible que no puedan hacer lo mismo para los tipos definidos por el usuario (que podría ser algo tan inocente como un iterador de lista). Una vez que te acostumbraste a hacer i++, se vuelve muy difícil recordar hacer ++i en cambio cuando i no es de un tipo incorporado (además, tendría que cambiar el código al cambiar un tipo), por lo que es mejor tener el hábito de usar siempre 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 sobrecarga del operador de la tercera regla básica: Si proporciona +, también proporcionar +=, si proporcionas -, no omitas -=, etc. Se dice que Andrew Koenig fue el primero en observar que los operadores de asignación compuesta se pueden utilizar como base para sus contrapartes no compuestos. Es decir, 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, debería ser miembro. 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 eficaz que devolver una copia, pero en el caso de operator+, no hay forma de evitar la copia. Cuando escribes a + b, espera que el resultado sea un nuevo valor, por lo que operator+ tiene que 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 que da para operator= tomando su argumento por copia.

Los operadores de manipulación de bits ~ & | ^ << >> debe implementarse de la misma manera que los operadores aritméticos. Sin embargo, (excepto por la sobrecarga << y >> para salida y entrada) hay muy pocos casos de uso razonables para sobrecargarlos.

3 Nuevamente, la lección que se debe aprender de esto es que a += b es, en general, más eficiente que a + b y debería preferirse si es posible.

Subíndice 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 proporcionarlos es la siguiente:

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 integrado, la variante const del operador debería devolver una copia en lugar de una referencia constante:

class X {
  value_type& operator[](index_type idx);
  value_type  operator[](index_type idx) const;
  // ...
};

Operadores para tipos tipo puntero

Para definir sus propios iteradores o punteros inteligentes, debe sobrecargar el operador de desreferencia del prefijo unario * y el operador de acceso a miembros 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, casi siempre necesitarán una versión constante y una no constante. Para el -> operador, si value_type es de class (o struct or union) tipo, otro operator->() se llama de forma recursiva, hasta que un operator->() devuelve un valor de tipo que no es de clase.

El operador unario de dirección de nunca debe sobrecargarse.

Para operator->*() ver esta pregunta. Rara vez se usa y, por lo tanto, rara vez se sobrecarga. De hecho, ni siquiera los iteradores lo sobrecargan.


Continúa con el primer párrafo, este debe ir al grano y resumir el mensaje clave respondiendo a las preguntas: ¿Quién? ¿Qué? ¿Cuándo? ¿Dónde? Y ¿por qué? No debe tener más de XNUMX-XNUMX frases y debe ir en negrita. Operadores de conversión

Respondido el 06 de junio de 19 a las 10:06

operator->() es en realidad extremadamente extraño. No es necesario devolver un value_type* - de hecho, puede devolver otro tipo de clase, siempre que el tipo de clase tenga un operator->(), que luego se llamará posteriormente. Esta llamada recursiva de operator->()s procede hasta que un value_type* se produce el tipo de retorno. ¡Locura! :) - j_random_hacker

No se trata exactamente de efectividad. Se trata de que no podemos hacerlo de la manera tradicional-idiomática en (muy) pocos casos: cuando la definición de ambos operandos debe permanecer sin cambios mientras calculamos el resultado. Y como dije, hay dos ejemplos clásicos: multiplicación de matrices y multiplicación de polinomios. Podríamos definir * en términos de *= pero sería incómodo porque una de las primeras operaciones de *= sería crear un nuevo objeto, resultado del cálculo. Luego, después del bucle for-ijk, intercambiaríamos este objeto temporal con *this. es decir. 1.copia, 2.operador *, 3.swap - Luc Hermitte

No estoy de acuerdo con las versiones const / non-const de sus operadores tipo puntero, por ejemplo, `const value_type & operator * () const;` - esto sería como tener un T* const devolviendo un const T& sobre la desreferenciación, que no es el caso. O en otras palabras: un puntero constante no implica un puntero constante. De hecho, no es trivial imitar T const * - que es la razón de todo const_iterator cosas en la biblioteca estándar. Conclusión: la firma debe ser reference_type operator*() const; pointer_type operator->() const - Arne Mertz

Un comentario: la implementación de los operadores aritméticos binarios sugeridos no es tan eficiente como podría ser. Nota de simmetría de los encabezados de los operadores de Se Boost: boost.org/doc/libs/1_54_0/libs/utility/operators.htm#simetría Se puede evitar una copia más si usa una copia local del primer parámetro, hace + = y devuelve la copia local. Esto permite la optimización NRVO. - Manu343726

Como mencioné en el chat, L <= R también se puede expresar como !(R < L) en lugar de !(L > R). Podría ahorrar una capa adicional de inserción en expresiones difíciles de optimizar (y también es la forma en que Boost.Operators lo implementa). - PlantillaRex

Las tres reglas básicas de la sobrecarga de operadores en C ++

Cuando se trata de sobrecarga de operadores en C ++, hay tres reglas básicas que debes seguir. Como ocurre con todas estas reglas, de hecho existen excepciones. A veces, las personas se han desviado de ellos y el resultado no fue un código incorrecto, pero estas desviaciones positivas son pocas y distantes entre sí. Como mínimo, 99 de cada 100 desviaciones de este tipo que he visto no estaban justificadas. Sin embargo, bien podría haber sido 999 de 1000. Así que será mejor que te ciñas a las siguientes reglas.

  1. 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 elegido.
    Básicamente, la primera y más importante regla para sobrecargar a los operadores, en su esencia, dice: No lo hagas. Eso puede parecer extraño, porque hay mucho que saber sobre la sobrecarga de operadores y, por lo tanto, muchos artículos, capítulos de libros y otros textos tratan de todo esto. Pero a pesar de esta evidencia aparentemente obvia, Sorprendentemente, hay pocos casos en los que la sobrecarga del operador sea 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. Contrariamente a la creencia popular, este casi nunca es el caso.

  2. Cíñete siempre a la conocida semántica del operador.
    C ++ no presenta limitaciones en la semántica de los operadores sobrecargados. Su compilador aceptará felizmente código que implemente el binario + operador para restar de su operando derecho. Sin embargo, los usuarios de dicho operador nunca sospecharían que la expresión a + b restar a del b. Por supuesto, esto supone que la semántica del operador en el dominio de la aplicación es indiscutible.

  3. Siempre proporcione todo de un conjunto de operaciones relacionadas.
    Los operadores están relacionados entre sí ya otras operaciones. Si su tipo admite a + b, los usuarios esperan poder llamar a += b, también. Si admite incremento de prefijo ++aellos esperarán a++ para trabajar también. Si pueden comprobar si a < b, seguramente también esperarán poder comprobar si a > b. Si pueden copiar-construir su tipo, esperan que la asignación también funcione.


Continúa con el primer párrafo, este debe ir al grano y resumir el mensaje clave respondiendo a las preguntas: ¿Quién? ¿Qué? ¿Cuándo? ¿Dónde? Y ¿por qué? No debe tener más de XNUMX-XNUMX frases y debe ir en negrita. La decisión entre miembro y no miembro.

Respondido el 20 de junio de 20 a las 10:06

Lo único de lo que soy consciente que viola cualquiera de estos es boost::spirit jajaja. - Billy ONeal

@Billy: Según algunos, abusar + porque la concatenación de cadenas es una violación, pero ahora se ha convertido en una praxis bien establecida, de modo que parece natural. Aunque recuerdo una clase de cuerdas caseras que vi en los 90 que usaba binario & para este propósito (refiriéndose a BASIC para praxis establecida). Pero, sí, ponerlo en la biblioteca estándar básicamente lo dejó en piedra. Lo mismo ocurre con el abuso << y >> para IO, BTW. ¿Por qué el desplazamiento a la izquierda sería la operación de salida obvia? Porque todos lo aprendimos cuando vimos nuestro primer "¡Hola, mundo!" solicitud. Y por ninguna otra razón. - sbi

@curiousguy: Si tienes que explicarlo, obviamente no es claro e indiscutible. Asimismo, si necesita discutir o defender la sobrecarga. - sbi

@sbi: la "revisión por pares" siempre es una buena idea. Para mí, un operador mal elegido no es diferente de un nombre de función mal elegido (vi muchos). El operador son solo funciones. Ni mas ni menos. Las reglas son las mismas. Y para entender si una idea es buena, la mejor forma es entender cuánto tiempo se tarda en entenderse. (Por lo tanto, la revisión por pares es imprescindible, pero los pares deben elegirse entre personas libres de dogmas y prejuicios). Emilio Garavaglia

@sbi Para mí, el único hecho absolutamente obvio e indiscutible sobre operator== es que debe ser una relación de equivalencia (IOW, no debe usar NaN sin señalización). Hay muchas relaciones de equivalencia útiles en contenedores. ¿Qué significa igualdad? "a iguales b" significa que a y b tienen el mismo valor matemático. El concepto de valor matemático de a (no NaN) float es claro, pero el valor matemático de un contenedor puede tener muchas definiciones útiles distintas (tipo recursivo). La definición más fuerte de igualdad es "son los mismos objetos", y es inútil. - curioso

La sintaxis general de la sobrecarga de operadores en C ++

No puede cambiar el significado de los operadores para los tipos integrados en C ++, los operadores solo se pueden sobrecargar para los tipos definidos por el usuario1. Es decir, al menos uno de los operandos debe ser de un tipo definido por el usuario. Al igual que con otras funciones sobrecargadas, los operadores se pueden sobrecargar para un determinado conjunto de parámetros solo una vez.

No todos los operadores se pueden sobrecargar en C ++. Entre los operadores que no se pueden sobrecargar se encuentran: . :: sizeof typeid .* y el único operador ternario en C ++, ?:

Entre los operadores que se pueden sobrecargar en C ++ se encuentran estos:

  • operadores aritméticos: + - * / % y += -= *= /= %= (todo infijo binario); + - (prefijo unario); ++ -- (prefijo unario y sufijo)
  • manipulación de bits: & | ^ << >> y &= |= ^= <<= >>= (todo infijo binario); ~ (prefijo unario)
  • álgebra de Boole: == != < > <= >= || && (todo infijo binario); ! (prefijo unario)
  • gestión de la memoria: new new[] delete delete[]
  • operadores de conversión implícitos
  • miscelánea: = [] -> ->* , (todo infijo binario); * & (todo prefijo unario) () (llamada de función, infijo n-ario)

Sin embargo, el hecho de que puede sobrecargar todos estos no significa que tú debemos hazlo. Consulte las reglas básicas de sobrecarga de operadores.

En C ++, los operadores están sobrecargados en forma de funciones con nombres especiales. Al igual que con otras funciones, los operadores sobrecargados generalmente se pueden implementar como un función miembro del tipo de su operando izquierdo o como funciones no miembros. El hecho de que tenga la libertad de elegir o esté obligado a utilizar cualquiera de ellos depende de varios criterios.2 Un operador unario @3, aplicado a un objeto x, se invoca como operator@(x) o como x.operator@(). Un operador de infijo binario @, aplicado a los objetos x y y, se llama como operator@(x,y) o como x.operator@(y).4

Los operadores que se implementan como funciones no miembros a veces son amigos del tipo de su operando.

1 El término "definido por el usuario" puede ser un poco engañoso. C ++ hace la distinción entre tipos integrados y tipos definidos por el usuario. A los primeros pertenecen, por ejemplo, int, char y double; a este último pertenecen todos los tipos de estructura, clase, unión y enumeración, incluidos los de la biblioteca estándar, aunque no estén, como tales, definidos por los usuarios.

2 Esto está cubierto en una parte posterior de estas preguntas frecuentes.

3 La @ 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 se puede sobrecargar y el único operador n-ario siempre debe implementarse como una función miembro.


Continúa con el primer párrafo, este debe ir al grano y resumir el mensaje clave respondiendo a las preguntas: ¿Quién? ¿Qué? ¿Cuándo? ¿Dónde? Y ¿por qué? No debe tener más de XNUMX-XNUMX frases y debe ir en negrita. Las tres reglas básicas de la sobrecarga de operadores en C ++.

Respondido el 20 de junio de 20 a las 10:06

~ es un prefijo unario, no un infijo binario. - mrkj

.* no se encuentra en la lista de operadores que no se pueden sobrecargar. - juglar celta

@ Mateen I deseado utilizar un marcador de posición en lugar de un operador real para dejar en claro que no se trata de un operador especial, sino que se aplica a todos ellos. Y, si quieres ser un programador de C ++, debes aprender a prestar atención incluso a la letra pequeña. :) - sbi

@HR: Si hubiera leído esta guía, sabría qué está mal. En general, sugiero que lea las tres primeras respuestas vinculadas a la pregunta. Eso no debería ser más de media hora de tu vida y te da una comprensión básica. La sintaxis específica del operador que puede buscar más tarde. Su problema específico sugiere que intente sobrecargar operator+() como una función miembro, pero le dio la firma de una función libre. Ver aquí. - sbi

@sbi: Ya leí las tres primeras publicaciones y gracias por hacerlas. :) Intentaré resolver el problema, de lo contrario, creo que es mejor plantearlo en una pregunta separada. ¡Gracias de nuevo por hacernos la vida tan fácil! :D - Hosein Rahnama

La decisión entre miembro y no miembro

Los operadores binarios = (asignación), [] (suscripción de matriz), -> (acceso de miembros), así como el n-ary () (llamada de función) operador, siempre debe implementarse como funciones miembro, porque la sintaxis del lenguaje lo requiere.

Otros operadores pueden implementarse como miembros o como no miembros. Algunas de ellas, sin embargo, normalmente tienen que implementarse como funciones no miembros, porque usted no puede modificar su operando izquierdo. Los más destacados son los operadores de entrada y salida. << y >>, cuyos operandos de la izquierda son clases de flujo de la biblioteca estándar que no puede cambiar.

Para todos los operadores en los que tiene que elegir implementarlos como una función miembro o una función no miembro, usa las siguientes reglas generales para decidir:

  1. Si es un operador unario, impleméntelo como un miembro función.
  2. Si un operador binario trata ambos operandos por igual (los deja sin cambios), implemente este operador como un no es miembro función.
  3. Si un operador binario lo hace no tratar sus dos operandos igualmente (normalmente cambiará su operando izquierdo), puede ser útil convertirlo en un miembro función del tipo de su operando izquierdo, si tiene que acceder a las partes privadas del operando.

Por supuesto, como ocurre con todas las reglas generales, hay excepciones. Si tienes un tipo

enum Month {Jan, Feb, ..., Nov, Dec}

y desea sobrecargar los operadores de incremento y decremento para ello, no puede hacer esto como funciones miembro, ya que en C ++, los tipos de enumeración no pueden tener funciones miembro. Entonces tienes que sobrecargarlo como una función gratuita. Y operator<() para una plantilla de clase anidada dentro de una plantilla de clase es mucho más fácil de escribir y leer cuando se hace como una función miembro en línea en la definición de clase. Pero estas son de hecho raras excepciones.

(Sin embargo, if haces una excepción, no olvides el tema de const-ness para el operando que, para funciones miembro, se convierte en el implícito this argumento. Si el operador como una función no miembro tomaría su argumento más a la izquierda como un const referencia, el mismo operador que una función miembro debe tener una const al final para hacer *this a const referencia.)


Continúa con el primer párrafo, este debe ir al grano y resumir el mensaje clave respondiendo a las preguntas: ¿Quién? ¿Qué? ¿Cuándo? ¿Dónde? Y ¿por qué? No debe tener más de XNUMX-XNUMX frases y debe ir en negrita. Operadores comunes para sobrecargar.

Respondido el 20 de junio de 20 a las 10:06

El artículo de Herb Sutter en Effective C ++ (¿o es C ++ Coding Standards?) Dice que se deberían preferir las funciones que no son miembros y que no son amigos a las funciones miembro, para aumentar la encapsulación de la clase. En mi humilde opinión, la razón de la encapsulación tiene prioridad sobre su regla de oro, pero no disminuye el valor de calidad de su regla de oro. - parcebal

@paercebal: C ++ eficaz es de Meyers, Estándares de codificación C ++ por Sutter. A cual te refieres? De todos modos, no me gusta la idea de, digamos, operator+=() no ser miembro. Tiene que cambiar su operando de la izquierda, por lo que, por definición, tiene que profundizar en sus entrañas. ¿Qué ganaría si no lo convirtiera en miembro? - sbi

@sbi: artículo 44 en estándares de codificación C ++ (Sutter) Prefiero escribir funciones no amigas que no sean miembros, por supuesto, solo se aplica si realmente puede escribir esta función usando solo la interfaz pública de la clase. Si no puede (o puede, pero obstaculizaría gravemente el desempeño), entonces debe convertirlo en miembro o amigo. - Matthieu M.

@sbi: Vaya, eficaz, excepcional ... No es de extrañar que mezcle los nombres. De todos modos la ganancia es limitar tanto como sea posible el número de funciones que tienen acceso a datos privados / protegidos de un objeto. De esta manera, aumenta la encapsulación de su clase, facilitando su mantenimiento / prueba / evolución. - parcebal

@sbi: Un ejemplo. Digamos que está codificando una clase String, con ambos operator += y append métodos. los append El método es más completo, porque puede agregar una subcadena del parámetro del índice i al índice n -1: append(string, start, end) Parece lógico tener += llamar anexar con start = 0 y end = string.size. En ese momento, append podría ser un método miembro, pero operator += no necesita ser miembro, y convertirlo en no miembro disminuiría la cantidad de código que se juega con las entrañas de String, por lo que es algo bueno .... ^ _ ^ ... - parcebal

Operadores de conversión (también conocidos como conversiones definidas por el usuario)

En C ++ puede crear operadores de conversión, operadores que permiten al compilador convertir entre sus tipos y otros tipos definidos. Hay dos tipos de operadores de conversión, implícitos y explícitos.

Operadores de conversión implícitos (C ++ 98 / C ++ 03 y C ++ 11)

Un operador de conversión implícita permite al compilador convertir implícitamente (como la conversión entre int y long) el valor de un tipo definido por el usuario a algún otro tipo.

La siguiente es una clase simple con un operador de conversión implícito:

class my_string {
public:
  operator const char*() const {return data_;} // This is the conversion operator
private:
  const char* data_;
};

Los operadores de conversión implícitos, como los constructores de un argumento, son conversiones definidas por el usuario. Los compiladores otorgarán una conversión definida por el usuario cuando intenten hacer coincidir una llamada con una función sobrecargada.

void f(const char*);

my_string str;
f(str); // same as f( str.operator const char*() )

Al principio, esto parece muy útil, pero el problema con esto es que la conversión implícita incluso se activa cuando no se espera que lo haga. En el siguiente código, void f(const char*) será llamado porque my_string() no es un lvalor, por lo que el primero no coincide:

void f(my_string&);
void f(const char*);

f(my_string());

Los principiantes se equivocan fácilmente e incluso los programadores de C ++ experimentados a veces se sorprenden porque el compilador elige una sobrecarga que no sospechaban. Estos problemas pueden mitigarse mediante operadores de conversión explícitos.

Operadores de conversión explícitos (C ++ 11)

A diferencia de los operadores de conversión implícitos, los operadores de conversión explícitos nunca se activan cuando no se espera que lo hagan. La siguiente es una clase simple con un operador de conversión explícito:

class my_string {
public:
  explicit operator const char*() const {return data_;}
private:
  const char* data_;
};

Note la explicit. Ahora, cuando intenta ejecutar el código inesperado de los operadores de conversión implícitos, obtiene un error del compilador:

prog.cpp: En la función 'int main ()': prog.cpp: 15: 18: error: no hay función coincidente para la llamada a 'f (my_string)' prog.cpp: 15: 18: nota: los candidatos son: prog. cpp: 11: 10: note: void f (my_string &) prog.cpp: 11: 10: note: no se conoce la conversión del argumento 1 de 'my_string' a 'my_string &' prog.cpp: 12: 10: note: void f ( const char *) prog.cpp: 12: 10: nota: no se conoce la conversión del argumento 1 de 'my_string' a 'const char *'

Para invocar el operador de conversión explícito, debe usar static_cast, un elenco de estilo C o un elenco de estilo constructor (es decir, T(value) ).

Sin embargo, hay una excepción a esto: el compilador puede convertir implícitamente a bool. Además, el compilador no puede realizar otra conversión implícita después de convertir a bool (un compilador puede hacer 2 conversiones implícitas a la vez, pero solo 1 conversión definida por el usuario como máximo).

Porque el compilador no emitirá "pasado" bool, los operadores de conversión explícitos ahora eliminan la necesidad de Idioma de Safe Bool. Por ejemplo, los punteros inteligentes anteriores a C ++ 11 usaban el modismo Safe Bool para evitar conversiones a tipos integrales. En C ++ 11, los punteros inteligentes usan un operador explícito en su lugar porque el compilador no puede convertir implícitamente a un tipo integral después de convertir explícitamente un tipo en bool.

Continúa con el primer párrafo, este debe ir al grano y resumir el mensaje clave respondiendo a las preguntas: ¿Quién? ¿Qué? ¿Cuándo? ¿Dónde? Y ¿por qué? No debe tener más de XNUMX-XNUMX frases y debe ir en negrita. Sobrecarga new y delete.

contestado el 23 de mayo de 17 a las 13:05

Sobrecarga new y delete

Nota: Esto solo se ocupa de la sintaxis de sobrecarga new y delete, no con el implementación de tales operadores sobrecargados. Creo que la semántica de la sobrecarga new y delete merecen sus propias preguntas frecuentes, dentro del tema de la sobrecarga de operadores, nunca podré hacerle justicia.

Eavor-Loop™

En C ++, cuando escribe un nueva expresión como new T(arg) Suceden dos cosas cuando se evalúa esta expresión: Primero operator new se invoca para obtener memoria en bruto, y luego el constructor apropiado de T se invoca para convertir esta memoria en bruto en un objeto válido. Del mismo modo, cuando elimina un objeto, primero se llama a su destructor y luego se devuelve la memoria a operator delete.
C ++ le permite ajustar estas dos operaciones: la gestión de la memoria y la construcción / destrucción del objeto en la memoria asignada. Esto último se realiza escribiendo constructores y destructores para una clase. La gestión de la memoria de ajuste fino se realiza escribiendo su propia operator new y operator delete.

La primera de las reglas básicas de la sobrecarga de operadores: no lo hagas - se aplica especialmente a la sobrecarga new y delete. Casi las únicas razones para sobrecargar a estos operadores son Problemas de rendimiento y restricciones de memoria, y en muchos casos, otras acciones, como cambios en los algoritmos usado, proporcionará mucho mayor relación costo / beneficio que intentar modificar la gestión de la memoria.

La biblioteca estándar de C ++ viene con un conjunto de new y delete operadores. Los más importantes son estos:

void* operator new(std::size_t) throw(std::bad_alloc); 
void  operator delete(void*) throw(); 
void* operator new[](std::size_t) throw(std::bad_alloc); 
void  operator delete[](void*) throw(); 

Los dos primeros asignan / desasignan memoria para un objeto, los dos últimos para una matriz de objetos. Si proporciona sus propias versiones de estos, no sobrecargar, sino reemplazar los de la biblioteca estándar.
Si te sobrecargas operator new, siempre debe sobrecargar la coincidencia operator delete, incluso si nunca tiene la intención de llamarlo. La razón es que, si un constructor arroja durante la evaluación de una nueva expresión, el sistema de tiempo de ejecución devolverá la memoria al operator delete coincidiendo con el operator new que se llamó para asignar la memoria para crear el objeto en. Si no proporciona una coincidencia operator delete, se llama al predeterminado, que casi siempre es incorrecto.
Si te sobrecargas new y delete, también debería considerar sobrecargar las variantes de la matriz.

Colocación provisional new

C ++ permite que los operadores nuevos y de eliminación tomen argumentos adicionales.
La denominada ubicación nueva le permite crear un objeto en una determinada dirección que se pasa a:

class X { /* ... */ };
char buffer[ sizeof(X) ];
void f()
{ 
  X* p = new(buffer) X(/*...*/);
  // ... 
  p->~X(); // call destructor 
} 

La biblioteca estándar viene con las sobrecargas apropiadas de los operadores new y delete para esto:

void* operator new(std::size_t,void* p) throw(std::bad_alloc); 
void  operator delete(void* p,void*) throw(); 
void* operator new[](std::size_t,void* p) throw(std::bad_alloc); 
void  operator delete[](void* p,void*) throw(); 

Tenga en cuenta que, en el código de ejemplo para la ubicación nueva dado anteriormente, operator delete nunca se llama, a menos que el constructor de X arroje una excepción.

También puedes sobrecargar new y delete con otros argumentos. Al igual que con el argumento adicional para la ubicación nueva, estos argumentos también se enumeran entre paréntesis después de la palabra clave new. Simplemente por razones históricas, estas variantes a menudo también se denominan colocación nueva, incluso si sus argumentos no son para colocar un objeto en una dirección específica.

Nuevos y eliminados específicos de la clase

Por lo general, querrá ajustar la administración de la memoria porque la medición ha demostrado que las instancias de una clase específica, o de un grupo de clases relacionadas, se crean y destruyen con frecuencia y que la administración de memoria predeterminada del sistema de tiempo de ejecución está ajustada para desempeño general, trata de manera ineficiente en este caso específico. Para mejorar esto, puede sobrecargar nuevo y eliminar para una clase específica:

class my_class { 
  public: 
    // ... 
    void* operator new();
    void  operator delete(void*,std::size_t);
    void* operator new[](size_t);
    void  operator delete[](void*,std::size_t);
    // ... 
}; 

Así, sobrecargado, new y delete se comportan como funciones miembro estáticas. Para objetos de my_class, la std::size_t el argumento siempre será sizeof(my_class). Sin embargo, estos operadores también se llaman para objetos asignados dinámicamente de clases derivadas, en cuyo caso podría ser mayor que eso.

Global nuevo y eliminar

Para sobrecargar el nuevo global y eliminarlo, simplemente reemplace los operadores predefinidos de la biblioteca estándar con los nuestros. Sin embargo, esto rara vez es necesario.

Respondido el 20 de junio de 20 a las 10:06

Tampoco estoy de acuerdo en que reemplazar el operador global new y delete sea generalmente por rendimiento: por el contrario, generalmente es para rastrear errores. - Ytrill

También debe tener en cuenta que si usa un nuevo operador sobrecargado, debe proporcionar también un operador de eliminación con argumentos coincidentes. Dices eso en la sección de global new / delete donde no es de mucho interés. - Ytrill

@Yttrill estás confundiendo las cosas. La sentido se sobrecarga. Lo que significa "sobrecarga del operador" es que el significado está sobrecargado. No significa que las funciones estén literalmente sobrecargadas y en particular operator new no sobrecargará la versión estándar. @sbi no afirma lo contrario. Es común llamarlo "sobrecargar nuevo" tanto como es común decir "sobrecargar operador de adición". - Johannes Schaub - litb

@sbi: Ver (o mejor, enlazar a) gotw.ca/publications/mill15.htm . Es solo una buena práctica para las personas que a veces usan nothrow nuevo. - Alexander C.

"Si no proporciona una eliminación de operador coincidente, la predeterminada se llama" -> En realidad, si agrega algún argumento y no crea una eliminación coincidente, no se llama a ninguna eliminación de operador y tiene una pérdida de memoria. (15.2.2, el almacenamiento ocupado por el objeto se desasigna solo si se encuentra una eliminación apropiada ... del operador) - dascandy

Porque no puedo operator<< función para transmitir objetos a std::cout o para que un archivo sea una función miembro?

Digamos que tienes:

struct Foo
{
   int a;
   double b;

   std::ostream& operator<<(std::ostream& out) const
   {
      return out << a << " " << b;
   }
};

Dado eso, no puede usar:

Foo f = {10, 20.0};
std::cout << f;

Como operator<< está sobrecargado como función miembro de Foo, el LHS del operador debe ser un Foo objeto. Lo que significa que se le pedirá que utilice:

Foo f = {10, 20.0};
f << std::cout

que es muy poco intuitivo.

Si lo define como una función no miembro,

struct Foo
{
   int a;
   double b;
};

std::ostream& operator<<(std::ostream& out, Foo const& f)
{
   return out << f.a << " " << f.b;
}

Podrás utilizar:

Foo f = {10, 20.0};
std::cout << f;

que es muy intuitivo.

contestado el 23 de mayo de 17 a las 13:05

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.