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

2145

Nota: Las respuestas se dieron en un orden específico , pero dado que muchos usuarios clasifican las respuestas de acuerdo con los votos, en lugar del momento en que se dieron, aquí hay un índice de las respuestas en el orden en que tienen más sentido:

(Nota: Esto está destinado a ser una entrada a las preguntas frecuentes de C ++ de Stack Overflow . Si desea criticar la idea de proporcionar una pregunta frecuente en este formulario, entonces la publicación en meta que comenzó todo esto sería el lugar para hacerlo. Respuestas a esa pregunta se monitorea en la sala de chat de C ++ , donde la idea de preguntas frecuentes comenzó en primer lugar, por lo que es muy probable que su respuesta sea leída por aquellos a quienes se les ocurrió la idea).

sbi
fuente
63
Si vamos a continuar con la etiqueta C ++ - Preguntas frecuentes, así es como se deben formatear las entradas.
John Dibling
He escrito una breve serie de artículos para la comunidad alemana de C ++ sobre la sobrecarga de operadores: Parte 1: la sobrecarga de operadores en C ++ cubre la semántica, el uso típico y las especialidades para todos los operadores. Tiene algunas superposiciones con sus respuestas aquí, sin embargo, hay información adicional. Las partes 2 y 3 hacen un tutorial para usar Boost.Operators. ¿Quieres que los traduzca y los agregue como respuestas?
Arne Mertz
Ah, y también hay disponible una traducción al inglés: conceptos básicos y práctica común
Arne Mertz

Respuestas:

1044

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 thisargumento 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 consten 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 ++icuando ino 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 + by 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_typees de class(o structo uniontipo), 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

sbi
fuente
89
operator->()En realidad es extremadamente raro. No es necesario devolver un value_type*- de hecho, puede devolver otro tipo de clase, siempre que ese tipo de clase tenga unoperator->() , que luego se llamará posteriormente. Esta llamada recursiva de operator->()s continúa hasta que se value_type*produce un tipo de retorno. ¡Locura! :)
j_random_hacker
2
No se trata exactamente de efectividad. Se trata de que no podemos hacerlo de la manera tradicional-idiomática en unos (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 *=crear un nuevo objeto, resultado del cálculo. Luego, después del ciclo for-ijk, intercambiaríamos este objeto temporal con *this. es decir. 1.copy, 2.operator *, 3.swap
Luc Hermitte
66
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* constretorno de una const T&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 el contenido const_iteratorde la biblioteca estándar. Conclusión: la firma debería serreference_type operator*() const; pointer_type operator->() const
Arne Mertz
66
Un comentario: la implementación de operadores aritméticos binarios sugeridos no es tan eficiente como puede ser. Nota de simetría de encabezados de operadores de Boost: boost.org/doc/libs/1_54_0/libs/utility/operators.htm#symmetry Se puede evitar una copia más si usa una copia local del primer parámetro, haga + = y devuelva copia local Esto permite la optimización NRVO.
Manu343726
3
Como mencioné en el chat, L <= Rtambién se puede expresar como en !(R < L)lugar de !(L > R). Podría ahorrar una capa adicional de línea en expresiones difíciles de optimizar (y también es cómo Boost.Operators lo implementa).
TemplateRex
494

Las tres reglas básicas de sobrecarga del operador en C ++

Cuando se trata de la sobrecarga del operador en C ++, hay tres reglas básicas que debe seguir . Como con todas esas reglas, de hecho hay excepciones. A veces las personas se han desviado de ellos y el resultado no fue un mal código, pero tales desviaciones positivas son pocas y distantes. Como mínimo, 99 de cada 100 desviaciones que he visto no estaban justificadas. Sin embargo, bien podría haber sido 999 de 1000. Por lo tanto, es mejor que cumpla con 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 esencia, dice: no lo hagas . Puede parecer extraño, porque hay mucho que saber sobre la sobrecarga del operador y muchos artículos, capítulos de libros y otros textos tratan de todo esto. Pero a pesar de esta evidencia aparentemente obvia, solo hay unos pocos casos sorprendentemente donde la sobrecarga del operador 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. Contrariamente a la creencia popular, este casi nunca es el caso.

  2. Siempre respete la conocida semántica del operador.
    C ++ no plantea limitaciones en la semántica de los operadores sobrecargados. Su compilador aceptará felizmente el código que implementa el+operadorbinariopara restar de su operando correcto. Sin embargo, los usuarios de dicho operador no se sospecharía la expresióna + bde restaraa partirb. Por supuesto, esto supone que la semántica del operador en el dominio de la aplicación es indiscutible.

  3. Proporcione siempre todo de un conjunto de operaciones relacionadas.
    Los operadores están relacionados entre sí y con otras operaciones. Si su tipo es compatiblea + b, los usuarios también podrán llamara += b. Si admite incremento de prefijo++a, esperarána++que funcione también. Si pueden verificar sia < b, seguramente esperarán también poder verificar sia > b. Si pueden copiar y construir su tipo, esperan que la asignación funcione también.


Continúe con la Decisión entre miembro y no miembro .

sbi
fuente
16
Lo único de lo que sé que viola cualquiera de estos es boost::spiritjajaja.
Billy ONeal
66
@Billy: Según algunos, abusar +de la concatenación de cadenas es una violación, pero ahora se ha convertido en una práctica bien establecida, por lo que parece natural. Aunque sí recuerdo una clase de cuerdas caseras que vi en los años 90 que usaba binarios &para este propósito (refiriéndose a BASIC para la praxis establecida). Pero, sí, ponerlo en la biblioteca estándar básicamente establece esto en piedra. Lo mismo ocurre con el abuso <<y >>para IO, por cierto. ¿Por qué el desplazamiento a la izquierda sería la operación de salida obvia? Porque todos lo supimos cuando vimos nuestro primer "¡Hola, mundo!" solicitud. Y por ninguna otra razón.
sbi
55
@ curiousguy: si tiene que explicarlo, obviamente no es claro e indiscutible. Del mismo modo, si necesita discutir o defender la sobrecarga.
sbi
55
@sbi: "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 iguales. Y para entender si una idea es buena, la mejor manera es entender cuánto tiempo lleva ser entendida. (Por lo tanto, la revisión por pares es imprescindible, pero los pares deben elegirse entre personas libres de dogmas y prejuicios.)
Emilio Garavaglia
55
@sbi Para mí, el único hecho absolutamente obvio e indiscutible 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 los contenedores. ¿Qué significa igualdad? " aigual b" significa eso ay btiene el mismo valor matemático. El concepto de valor matemático de a (no NaN) floates claro, pero el valor matemático de un contenedor puede tener muchas definiciones útiles distintas (recursivas de tipo). La definición más fuerte de igualdad es "son los mismos objetos", y es inútil.
curiousguy
265

La sintaxis general de la sobrecarga del operador 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 1 definidos por el usuario . 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 pueden sobrecargarse para un determinado conjunto de parámetros solo 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 ++ se encuentran estos:

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

Sin embargo, el hecho de que tú pueda sobrecargar todo esto no significa que deba hacerlo. Consulte las reglas básicas de sobrecarga del operador.

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 una función miembro del tipo de su operando izquierdo o como funciones no miembros . Si es libre de elegir o está obligado a usar 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 infijo binario @, aplicado a los objetos xyy , se llama como operator@(x,y)o como x.operator@(y). 4 4

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

1 El término "definido por el usuario" puede ser ligeramente engañoso. C ++ hace la distinción entre los tipos integrados y los 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 El @no es un operador válido en C ++, por eso lo uso como marcador de posición.

4 4 El único operador ternario en C ++ no se puede sobrecargar y el único operador n-ary siempre debe implementarse como una función miembro.


Continúe con Las tres reglas básicas de sobrecarga del operador en C ++ .

sbi
fuente
~es un prefijo unario, no un infijo binario.
mrkj
1
.*falta en la lista de operadores no sobrecargables.
celticminstrel
1
@Mateen Quería usar 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
1
@HR: Si hubiera leído esta guía, sabría lo que 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 consultar más adelante. Su problema específico sugiere que intente sobrecargarse operator+()como una función miembro, pero le dio la firma de una función libre. Ver aquí .
sbi
1
@sbi: Ya leí las tres primeras publicaciones y gracias por hacerlas. :) Intentaré resolver el problema; de lo contrario, creo que es mejor preguntarlo por separado. ¡Gracias nuevamente por hacernos la vida tan fácil! : D
Hosein Rahnama
251

La decisión entre miembro y no miembro

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

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

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

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

Por supuesto, como 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, no puede hacer esto como funciones miembro, ya que en C ++, los tipos de enumeración no pueden tener funciones miembro. Por lo tanto, debe 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 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, si hace una excepción, no olvide la cuestión de const-ness para el operando que, para las funciones miembro, se convierte en el thisargumento implícito . Si el operador como una función no miembro tomaría su argumento más a la izquierda como constreferencia , el mismo operador que una función miembro debe tener un constal final para hacer *thisuna constreferencia).


Continuar a operadores comunes para sobrecargar .

sbi
fuente
99
El elemento de Herb Sutter en Effective C ++ (¿o son los estándares de codificación C ++?) Dice que uno debería preferir las funciones no miembro no amigo a las funciones miembro, para aumentar la encapsulación de la clase. En mi humilde opinión, la razón de encapsulación tiene prioridad sobre su regla general, pero no disminuye el valor de calidad de su regla general.
paercebal
8
@paercebal: C ++ efectivo es de Meyers, C ++ Coding Standards de Sutter. A cual te refieres? De todos modos, no me gusta la idea de, por ejemplo, operator+=()no ser miembro. Tiene que cambiar su operando de la izquierda, por lo que, por definición, tiene que cavar profundamente en sus entrañas. ¿Qué ganarías al no hacerlo miembro?
sbi
99
@sbi: Elemento 44 en los estándares de codificación de C ++ (Sutter). Prefiere escribir funciones que no sean miembros no amigos , por supuesto, solo se aplica si realmente puede escribir esta función utilizando solo la interfaz pública de la clase. Si no puede (o puede pero dificultaría mucho el rendimiento), debe hacerlo miembro o amigo.
Matthieu M.
3
@sbi: Vaya, efectivo, excepcional ... No es de extrañar que mezcle los nombres. De todos modos, la ganancia es limitar lo más posible el número de funciones que tienen acceso a un objeto privado / datos protegidos. De esta manera, aumenta la encapsulación de su clase, facilitando su mantenimiento / prueba / evolución.
paercebal
12
@sbi: un ejemplo. Digamos que está codificando una clase String, tanto con operator +=el appendmétodo como con el método. El appendmé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 que la +=llamada se agregue con start = 0y end = string.size. En ese momento, append podría ser un método de miembro, pero operator +=no necesita ser un miembro, y convertirlo en un no miembro disminuiría la cantidad de código que se reproduce con las entrañas de String, por lo que es algo bueno ... ^ _ ^ ...
paercebal
165

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

En C ++ puede crear operadores de conversión, operadores que permiten que el compilador convierta 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ícita (C ++ 98 / C ++ 03 y C ++ 11)

Un operador de conversión implícita permite que el compilador convierta implícitamente (como la conversión entre intylong ) el valor de un tipo definido por el usuario a 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ícita, 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*)se llamará porque my_string()no es un lvalue , 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 experimentados de C ++ a veces se sorprenden porque el compilador elige una sobrecarga que no sospechaban. Estos problemas pueden ser mitigados por 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 activarán cuando no esperes 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_;
};

Observe 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: nota: nulo f (my_string &)
prog.cpp: 11: 10: nota: no se conoce la conversión para el argumento 1 de 'my_string' a 'my_string &'
prog.cpp: 12: 10: nota: void f (const char *)
prog.cpp: 12: 10: nota: no se conoce la conversión para el argumento 1 de 'my_string' a 'const char *'

Para invocar el operador de conversión explícito, debe usar static_castuna conversión de estilo C o una conversión de estilo de 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 hacer otra conversión implícita después de que se convierte 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).

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

Continuar a Sobrecarga newydelete .

JKor
fuente
148

Sobrecarga newydelete

Nota: Esto solo trata con la sintaxis de sobrecarganewydeleteno con la implementación de dichos operadores sobrecargados. Creo que la semántica de sobrecargarnew y deletemerecer sus propias preguntas frecuentes , dentro del tema de la sobrecarga del operador, nunca puedo hacerle justicia.

Lo esencial

En C ++, cuando se escribe una nueva expresión como new T(arg)dos cosas suceden cuando se evalúa esta expresión: En primer lugar operator newse invoca para obtener la memoria prima, y luego el constructor apropiado de Tse 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 operator delete.
C ++ le permite ajustar ambas operaciones: administración de memoria y construcción / destrucción del objeto en la memoria asignada. Esto último se hace escribiendo constructores y destructores para una clase. La gestión de la memoria de ajuste fino se realiza escribiendo su propio operator newy operator delete.

La primera de las reglas básicas de sobrecarga del operador, no lo haga, se aplica especialmente a la sobrecarga newy delete. Casi las únicas razones para sobrecargar estos operadores son los problemas de rendimiento y las limitaciones de memoria , y en muchos casos, otras acciones, como los cambios en los algoritmos utilizados, proporcionarán una relación costo / ganancia mucho mayor que intentar modificar la administración de memoria.

La biblioteca estándar de C ++ viene con un conjunto de operadores newy deleteoperadores predefinidos . 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 estas, no se sobrecargarán, sino que reemplazarán las de la biblioteca estándar.
Si sobrecarga operator new, siempre debe sobrecargar la coincidencia operator delete, incluso si nunca tiene la intención de llamarla. La razón es que, si un constructor lanza durante la evaluación de una nueva expresión, el sistema de tiempo de ejecución devolverá la memoria a la operator deletecoincidencia operator newque se llamó para asignar la memoria para crear el objeto. Si no proporciona una coincidencia operator delete, se llama al predeterminado, que casi siempre es incorrecto.
Si sobrecarga newy delete, también debería considerar sobrecargar las variantes de la matriz.

Colocación new

C ++ permite que los operadores nuevos y eliminados tomen argumentos adicionales.
La llamada nueva ubicación 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 nuevos y eliminados 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 colocación de nuevos dados anteriormente, operator deletenunca se llama, a menos que el constructor de X arroje una excepción.

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

Específico de clase nuevo y eliminar

Lo más común es que desee ajustar la administración de 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 a menudo y que la administración de memoria predeterminada del sistema de tiempo de ejecución, ajustada para rendimiento general, trata de manera ineficiente en este caso específico. Para mejorar esto, puede sobrecargar new y delete 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);
    // ... 
}; 

Sobrecargado así, new y delete se comportan como funciones miembro estáticas. Para objetos de my_class, el std::size_targumento siempre será sizeof(my_class). Sin embargo, estos operadores también son llamados 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 y eliminar global, simplemente reemplace los operadores predefinidos de la biblioteca estándar con los nuestros. Sin embargo, esto rara vez debe hacerse.

sbi
fuente
11
Tampoco estoy de acuerdo en que reemplazar el operador global nuevo y eliminar sea generalmente por rendimiento: por el contrario, generalmente es por rastreo de errores.
Yttrill
1
También debe tener en cuenta que si usa un operador nuevo sobrecargado, también debe proporcionar un operador de eliminación con argumentos coincidentes. Dices eso en la sección sobre global nuevo / eliminar donde no es de mucho interés.
Yttrill
13
@ Aún estás confundiendo cosas. El significado se sobrecarga. Lo que significa "sobrecarga del operador" es que el significado está sobrecargado. No significa que, literalmente, las funciones estén sobrecargadas, y en particular el operador nuevo no sobrecargará la versión del Estándar. @sbi no dice lo contrario. Es común llamarlo "sobrecarga de nuevo" tanto como es común decir "operador de adición de sobrecarga".
Johannes Schaub - litb
1
@sbi: Ver (o mejor, enlace a) gotw.ca/publications/mill15.htm . Es solo una buena práctica hacia las personas que a veces usan nothrownuevas.
Alexandre C.
1
"Si no proporciona una eliminación de operador coincidente, la predeterminada se llama" -> En realidad, si agrega argumentos 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 de operador apropiada)
dascandy
46

¿Por qué no puede operator<<funcionar std::coutuna función miembro para transmitir objetos a un archivo?

Digamos que tienes:

struct Foo
{
   int a;
   double b;

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

Dado eso, no puedes usar:

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

Como operator<<se sobrecarga como una función miembro de Foo, el LHS del operador debe ser un Fooobjeto. Lo que significa que se le pedirá que use:

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

lo cual es muy no 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 usar:

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

lo cual es muy intuitivo

R Sahu
fuente