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




sobrecarga de operadores poo (5)

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

No puede cambiar el significado de los operadores para los tipos incorporados en C ++, los operadores solo pueden sobrecargarse 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 una sola vez.

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

Entre los operadores que pueden sobrecargarse 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 booleana: == != < > <= >= || && (todo infijo binario); ! (prefijo unario)
  • gestión de memoria: new new[] delete delete[]
  • operadores de conversión implícita
  • miscelánea: = [] -> ->* , (todos los infijos binarios); * & (todos los prefijos unarios) () (llamada de función, infijo n-ary)

Sin embargo, el hecho de que pueda sobrecargar todo esto no significa que deba hacerlo. Ver 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 pueden implementarse como una función miembro del tipo de su operando izquierdo o como funciones no miembros . Si usted es libre de elegir o está obligado a usar cualquiera de los dos, depende de varios criterios. 2 Un operador unario @ 3 , aplicado a un objeto x, se invoca como [email protected](x) o como [email protected]() . Un operador de infijo binario @ , aplicado a los objetos y , se llama como [email protected](x,y) o como [email protected](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 incorporados 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 definidos, como tales, por los usuarios.

2 Esto se trata en una parte posterior de esta pregunta frecuente.

3 El @ no es un operador válido en C ++, por eso 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úe con las tres reglas básicas de sobrecarga de operadores en C ++ .

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

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


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

Cuando se trata de la sobrecarga de operadores en C ++, hay tres reglas básicas que debe seguir . Al igual que con todas estas reglas, de hecho hay excepciones. Algunas veces las personas se han desviado de ellos y el resultado no fue un código malo, pero tales desviaciones positivas son pocas y distantes entre sí. Por lo menos, 99 de cada 100 desviaciones que he visto estaban injustificadas. Sin embargo, podría haber sido 999 de 1000. Así que es mejor que te limites a las siguientes reglas.

  1. Siempre que el significado de un operador no sea claro e indiscutible, no debe sobrecargarse. En su lugar, proporcionar una función con un nombre bien elegido.
    Básicamente, la primera y más importante regla para sobrecargar a los 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 lo tanto, muchos artículos, capítulos de libros y otros textos tratan todo esto. Pero a pesar de esta evidencia aparentemente obvia, hay solo unos pocos casos sorprendentemente apropiados donde 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. Contrariamente a la creencia popular, este casi nunca es el caso.

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

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

Continuar con la decisión entre el miembro y el no miembro .


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, los implícitos y los explícitos.

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

Un operador de conversión implícita le 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í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. En el siguiente código, se llamará void f(const char*) 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 escoge una sobrecarga que no sospecharon. 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 espere 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 lo explicit . Ahora, cuando intenta ejecutar el código inesperado de los operadores de conversión implícitos, obtiene un error del compilador:

prog.cpp: In function ‘int main()’:
prog.cpp:15:18: error: no matching function for call to ‘f(my_string)’
prog.cpp:15:18: note: candidates are:
prog.cpp:11:10: note: void f(my_string&)
prog.cpp:11:10: note:   no known conversion for argument 1 from ‘my_string’ to ‘my_string&’
prog.cpp:12:10: note: void f(const char*)
prog.cpp:12:10: note:   no known conversion for argument 1 from ‘my_string’ to ‘const char*’

Para invocar el operador de static_cast explícito, debe usar static_cast , una static_cast de estilo C o una static_cast de estilo de constructor (es decir, T(value) ).

Sin embargo, hay una excepción a esto: el compilador tiene permitido convertir implícitamente a bool . Además, al compilador no se le permite hacer otra conversión implícita después de que se convierta a bool (a un compilador se le permite 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 emitirá bools "pasados", los operadores de conversión explícitos ahora eliminan la necesidad del idioma Safe Bool . Por ejemplo, los punteros inteligentes antes de C ++ 11 utilizaban el lenguaje 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 que haya convertido explícitamente un tipo a bool.

Continuar con la sobrecarga de new y delete .


Sobrecarga de new y delete

Nota: Esto solo trata con la sintaxis de la sobrecarga new y la delete , no con la implementación de dichos operadores sobrecargados. Creo que la semántica de sobrecargar new y delete merece sus propias preguntas frecuentes , dentro del tema de la sobrecarga de operadores, nunca podré hacer justicia.

Lo esencial

En C ++, cuando escribe una nueva expresión como new T(arg) , suceden dos cosas cuando se evalúa esta expresión: el primer operator new se invoca para obtener memoria en bruto, y luego se invoca al constructor apropiado de T para convertir esta memoria en bruto en un objeto valido Del mismo modo, cuando elimina un objeto, primero se llama a su destructor, y luego la memoria se devuelve al operator delete .
C ++ le permite ajustar ambas operaciones: administración de la memoria y la construcción / destrucción del objeto en la memoria asignada. Lo ú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 new y el operator delete .

La primera de las reglas básicas de la sobrecarga de operadores, no lo haga, se aplica especialmente a la sobrecarga de new y delete . Casi las únicas razones para sobrecargar a estos operadores son problemas de rendimiento y 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 ++ incluye un conjunto predefinido de operadores new y de delete . 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 se sobrecargarán, sino que reemplazarán a los de la biblioteca estándar.
Si sobrecarga al operator new , siempre debe sobrecargar también la operator delete correspondiente, incluso si nunca tiene la intención de llamarlo. 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 al operator delete coincidiendo con el operator new que se llamó para asignar la memoria para crear el objeto. Si lo hace no proporciona una operator delete coincidente, se llama al predeterminado, que casi siempre es incorrecto.
Si sobrecarga de new y 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 ubicación nueva le permite crear un objeto en una dirección determinada 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 de eliminación 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 dada anteriormente, la operator delete nunca se llama, a menos que el constructor de X emita una excepción.

También puede 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 llaman ubicación nueva, incluso si sus argumentos no son para colocar un objeto en una dirección específica.

Clase específica nueva y eliminar

Lo más común es que desee 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 a menudo y que la administración de la memoria predeterminada del sistema de tiempo de ejecución, optimizada para El desempeño general, trata ineficientemente 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);
    // ... 
}; 

Sobrecargado por lo tanto, nuevo y eliminar 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 lo nuevo global y eliminar, simplemente reemplace los operadores predefinidos de la biblioteca estándar con los nuestros. Sin embargo, esto rara vez debe hacerse.


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

Digamos que tienes:

struct Foo
{
   int a;
   double b;

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

Dado que, no puedes 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 Fooobjeto. Lo que significa, se le pedirá que utilice:

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 utilizar:

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

Lo cual es muy intuitivo.





c++-faq