La sobrecarga de operadores en programación es una de las formas de implementar el polimorfismo , que consiste en la posibilidad de la existencia simultánea en un mismo ámbito de varias opciones diferentes para utilizar operadores que tienen el mismo nombre, pero difieren en los tipos de parámetros a los que están sujetos. aplicado.
El término " sobrecarga " es un papel de calco de la palabra inglesa sobrecarga . Tal traducción apareció en libros sobre lenguajes de programación en la primera mitad de la década de 1990. En las publicaciones del período soviético, mecanismos similares se denominaron redefinición o redefinición , operaciones superpuestas .
A veces existe la necesidad de describir y aplicar operaciones a los tipos de datos creados por el programador que tienen un significado equivalente a los que ya están disponibles en el lenguaje. Un ejemplo clásico es la biblioteca para trabajar con números complejos . Ellos, como los tipos numéricos ordinarios, admiten operaciones aritméticas, y sería natural crear para este tipo de operaciones "más", "menos", "multiplicar", "dividir", denotándolos con los mismos signos de operación que para otros números. tipos La prohibición del uso de elementos definidos en el lenguaje obliga a la creación de muchas funciones con nombres como ComplexPlusComplex, IntegerPlusComplex, ComplexMinusFloat, etc.
Cuando se aplican operaciones del mismo significado a operandos de diferentes tipos, se les obliga a tener nombres diferentes. La imposibilidad de utilizar funciones con el mismo nombre para diferentes tipos de funciones lleva a la necesidad de inventar diferentes nombres para lo mismo, lo que crea confusión e incluso puede dar lugar a errores. Por ejemplo, en el lenguaje C clásico, hay dos versiones de la función de biblioteca estándar para encontrar el módulo de un número: abs() y fabs() - la primera es para un argumento entero, la segunda para uno real. Esta situación, combinada con una verificación de tipo C débil, puede llevar a un error difícil de encontrar: si un programador escribe abs(x) en el cálculo, donde x es una variable real, entonces algunos compiladores generarán código sin previo aviso que convierta x en un número entero descartando las partes fraccionarias y calcule el módulo a partir del número entero resultante.
En parte, el problema se resuelve mediante la programación de objetos: cuando los nuevos tipos de datos se declaran como clases, las operaciones sobre ellos se pueden formalizar como métodos de clase, incluidos los métodos de clase del mismo nombre (ya que los métodos de diferentes clases no tienen que tener nombres diferentes), pero, en primer lugar, tal forma de diseño de operaciones en valores de diferentes tipos es inconveniente y, en segundo lugar, no resuelve el problema de crear nuevos operadores.
Las herramientas que le permiten expandir el lenguaje, complementarlo con nuevas operaciones y construcciones sintácticas (y la sobrecarga de operaciones es una de esas herramientas, junto con objetos, macros, funcionales, cierres) lo convierten en un metalenguaje , una herramienta para describir lenguajes. centrado en tareas específicas. Con su ayuda, es posible construir una extensión de lenguaje para cada tarea específica que sea más apropiada para ella, lo que permitirá describir su solución de la forma más natural, comprensible y simple. Por ejemplo, en una aplicación para operaciones de sobrecarga: crear una biblioteca de tipos matemáticos complejos (vectores, matrices) y describir las operaciones con ellos en una forma natural, "matemática", crea un "lenguaje para operaciones vectoriales", en el que la complejidad de Los cálculos están ocultos, y es posible describir la solución de problemas en términos de operaciones vectoriales y matriciales, centrándose en la esencia del problema, no en la técnica. Fue por estas razones que tales medios alguna vez se incluyeron en el lenguaje Algol-68 .
La sobrecarga de operadores implica la introducción de dos características interrelacionadas en el lenguaje: la capacidad de declarar varios procedimientos o funciones con el mismo nombre en el mismo ámbito y la capacidad de describir sus propias implementaciones de operadores binarios (es decir, los signos de operaciones, generalmente escrito en notación infija, entre operandos). Básicamente, su implementación es bastante simple:
Hay cuatro tipos de sobrecarga de operadores en C++:
Es importante recordar que la sobrecarga mejora el idioma, no cambia el idioma, por lo que no puede sobrecargar operadores para tipos integrados. No puede cambiar la precedencia y la asociatividad (de izquierda a derecha o de derecha a izquierda) de los operadores. No puede crear sus propios operadores y sobrecargar algunos de los integrados: :: . .* ?: sizeof typeid. Además, los operadores && || ,pierden sus propiedades únicas cuando se sobrecargan: pereza para los dos primeros y precedencia para una coma (el orden de las expresiones entre comas se define estrictamente como asociativo a la izquierda, es decir, de izquierda a derecha). El operador ->debe devolver un puntero o un objeto (por copia o referencia).
Los operadores se pueden sobrecargar como funciones independientes y como funciones miembro de una clase. En el segundo caso, el argumento izquierdo del operador es siempre el objeto *este. Los operadores = -> [] ()solo se pueden sobrecargar como métodos (funciones miembro), no como funciones.
Puede hacer que escribir código sea mucho más fácil si sobrecarga los operadores en un orden determinado. Esto no solo acelerará la escritura, sino que también evitará que dupliques el mismo código. Consideremos una sobrecarga usando el ejemplo de una clase que es un punto geométrico en un espacio vectorial bidimensional:
punto de clase _ { int x , y ; público : Punto ( int x , int xx ) : x ( x ), y ( xx ) {} // El constructor predeterminado se ha ido. // Los nombres de los argumentos de los constructores pueden ser los mismos que los nombres de los campos de clase. }Otros operadores no están sujetos a ninguna directriz general de sobrecarga.
Tipo de conversionesLas conversiones de tipo le permiten especificar las reglas para convertir nuestra clase a otros tipos y clases. También puede especificar el especificador explícito, que permitirá la conversión de tipos solo si el programador lo especificó explícitamente (por ejemplo , static_cast<Point3>(Point(2,3)); ). Ejemplo:
Punto :: operador bool () const { devuelve esto -> x != 0 || esto -> y != 0 ; } Operadores de asignación y desasignaciónLos operadores new new[] delete delete[]pueden estar sobrecargados y pueden tomar cualquier número de argumentos. Además, los operadores new и new[]deben tomar un argumento de tipo como primer argumento std::size_ty devolver un valor de tipo void *, y los operadores deben tomar el delete delete[]primero void *y no devolver nada ( void). Estos operadores se pueden sobrecargar tanto para funciones como para clases concretas.
Ejemplo:
void * MiClase :: operador nuevo ( std :: size_t s , int a ) { vacío * p = malloc ( s * a ); si ( p == punto nulo ) lanzar "¡No hay memoria libre!" ; devuelve p ; } // ... // Llamada: MiClase * p = new ( 12 ) MiClase ;
Los literales personalizados existen desde el undécimo estándar de C++. Los literales se comportan como funciones regulares. Pueden ser calificadores en línea o constexpr . Es deseable que el literal comience con un carácter de subrayado, ya que puede haber un conflicto con estándares futuros. Por ejemplo, el literal i ya pertenece a los números complejos de std::complex.
Los literales pueden tomar sólo uno de los siguientes tipos: const char * , unsigned long long int , long double , char , wchar_t , char16_t , char32_t. Basta con sobrecargar el literal solo para el tipo const char * . Si no se encuentra un candidato más adecuado, se llamará a un operador con ese tipo. Un ejemplo de conversión de millas a kilómetros:
constexpr int operador "" _mi ( int largo largo sin signo i ) { retornar 1.6 * i ;} operador doble constexpr "" _mi ( long double i ) { retornar 1.6 * i ;}Los literales de cadena toman un segundo argumento std::size_ty uno de los primeros: const char * , const wchar_t *, const char16_t * , const char32_t *. Los literales de cadena se aplican a las entradas entre comillas dobles.
C++ tiene un literal R de cadena de prefijo incorporado que trata todos los caracteres entre comillas como caracteres regulares y no interpreta ciertas secuencias como caracteres especiales. Por ejemplo, dicho comando std::cout << R"(Hello!\n)"mostrará Hello!\n.
La sobrecarga de operadores está estrechamente relacionada con la sobrecarga de métodos. Un operador está sobrecargado con la palabra clave Operador, que define un "método de operador", que, a su vez, define la acción del operador con respecto a su clase. Hay dos formas de métodos de operador (operador): uno para operadores unarios , el otro para operadores binarios . A continuación se muestra la forma general para cada variación de estos métodos.
// forma general de sobrecarga de operador unario. public static return_type operator op ( operando de tipo_parámetro ) { // operaciones } // Forma general de sobrecarga de operadores binarios. public static return_type operator op ( parametro_tipo1 operando1 , parametro_tipo2 operando2 ) { // operaciones }Aquí, en lugar de "op", se sustituye un operador sobrecargado, por ejemplo + o /; y "return_type" denota el tipo específico de valor devuelto por la operación especificada. Este valor puede ser de cualquier tipo, pero a menudo se especifica que sea del mismo tipo que la clase para la que se sobrecarga el operador. Esta correlación facilita el uso de operadores sobrecargados en expresiones. Para los operadores unarios, el operando denota el operando que se está pasando, y para los operadores binarios, lo mismo se denota por "operando1 y operando2". Tenga en cuenta que los métodos de operador deben ser de ambos tipos, públicos y estáticos. El tipo de operando de los operadores unarios debe ser el mismo que el de la clase para la que se sobrecarga el operador. Y en los operadores binarios, al menos uno de los operandos debe ser del mismo tipo que su clase. Por lo tanto, C# no permite que ningún operador se sobrecargue en objetos que aún no se han creado. Por ejemplo, la asignación del operador + no se puede anular para elementos de tipo int o string . No puede usar el modificador ref o out en los parámetros del operador. [una]
La sobrecarga de procedimientos y funciones al nivel de una idea general, por regla general, no es difícil de implementar ni de comprender. Sin embargo, incluso en él hay algunas "trampas" que deben ser consideradas. Permitir la sobrecarga de operadores crea muchos más problemas tanto para el implementador del lenguaje como para el programador que trabaja en ese lenguaje.
Problema de identificaciónEl primer problema es la dependencia del contexto . Es decir, la primera pregunta que enfrenta un desarrollador de un traductor de lenguaje que permite la sobrecarga de procedimientos y funciones es: ¿cómo elegir entre los procedimientos del mismo nombre el que se debe aplicar en este caso particular? Todo está bien si existe una variante del procedimiento cuyos tipos de parámetros formales coincidan exactamente con los tipos de los parámetros reales utilizados en esta llamada. Sin embargo, en casi todos los lenguajes, existe cierto grado de libertad en el uso de tipos, asumiendo que el compilador en ciertas situaciones convierte automáticamente (emite) tipos de datos de manera segura. Por ejemplo, en operaciones aritméticas con argumentos reales y enteros, el tipo entero generalmente se convierte automáticamente en tipo real y el resultado es real. Supongamos que hay dos variantes de la función de suma:
suma int(int a1, int a2); agregar flotante (flotante a1, flotante a2);¿Cómo debe manejar el compilador la expresión y = add(x, i)donde x es de tipo float e i es de tipo int? Obviamente no hay una coincidencia exacta. Hay dos opciones: ya sea y=add_int((int)x,i)o como (aquí , la primera y la segunda versión de la función se indican con y=add_flt(x, (float)i)los nombres add_inty respectivamente).add_flt
Surge la pregunta: ¿debería el compilador permitir este uso de funciones sobrecargadas y, de ser así, sobre qué base elegirá la variante particular utilizada? En particular, en el ejemplo anterior, ¿debería el traductor considerar el tipo de variable y al elegir? Cabe señalar que la situación dada es la más simple. Pero son posibles casos mucho más complicados, que se ven agravados por el hecho de que no solo los tipos incorporados se pueden convertir de acuerdo con las reglas del lenguaje, sino que también las clases declaradas por el programador, si tienen relaciones de parentesco, se pueden convertir de de uno a otro. Hay dos soluciones para este problema:
A diferencia de los procedimientos y funciones, las operaciones infijas de los lenguajes de programación tienen dos propiedades adicionales que afectan significativamente su funcionalidad: prioridad y asociatividad , cuya presencia se debe a la posibilidad de registro de operadores en "cadena" (cómo entender a+b*c : cómo (a+b)*co cómo a+(b*c)?Expresión a-b+c - esto (a-b)+co a-(b+c)?).
Las operaciones integradas en el lenguaje siempre tienen precedencia y asociatividad tradicionales predefinidas. Surge la pregunta: ¿qué prioridades y asociatividad tendrán las versiones redefinidas de estas operaciones, o más aún, las nuevas operaciones creadas por el programador? Hay otras sutilezas que pueden requerir aclaración. Por ejemplo, en C hay dos formas de los operadores de incremento y decremento ++y -- , prefijo y posfijo, que se comportan de manera diferente. ¿Cómo deben comportarse las versiones sobrecargadas de dichos operadores?
Diferentes idiomas tratan estos temas de diferentes maneras. Por lo tanto, en C++, la precedencia y la asociatividad de las versiones sobrecargadas de los operadores se conservan igual que las de los predefinidos en el lenguaje, y las descripciones sobrecargadas de las formas de prefijo y posfijo de los operadores de incremento y decremento usan diferentes firmas:
forma de prefijo | Forma de sufijo | |
---|---|---|
Función | T&operador ++(T&) | Operador T ++(T &, int) |
función miembro | T&T::operador ++() | TT::operador ++(int) |
De hecho, la operación no tiene un parámetro entero, es ficticio y se agrega solo para marcar la diferencia en las firmas.
Una pregunta más: ¿es posible permitir la sobrecarga de operadores para tipos de datos integrados y ya declarados? ¿Puede un programador cambiar la implementación de la operación de suma para el tipo de entero integrado? ¿O para el tipo de biblioteca "matriz"? Por regla general, la primera pregunta se responde negativamente. Cambiar el comportamiento de las operaciones estándar para tipos incorporados es una acción extremadamente específica, cuya necesidad real puede surgir solo en casos excepcionales, mientras que las consecuencias dañinas del uso incontrolado de dicha función son difíciles incluso de predecir por completo. Por lo tanto, el lenguaje generalmente prohíbe la redefinición de operaciones para tipos incorporados o implementa un mecanismo de sobrecarga de operadores de tal manera que las operaciones estándar simplemente no pueden anularse con su ayuda. En cuanto a la segunda pregunta (operadores de redefinición ya descritos para tipos existentes), el mecanismo de herencia de clases y anulación de métodos proporciona la funcionalidad necesaria: si desea cambiar el comportamiento de una clase existente, debe heredarla y redefinirla. los operadores descritos en él. En este caso, la clase anterior permanecerá sin cambios, la nueva recibirá la funcionalidad necesaria y no se producirán colisiones.
Anuncio de nuevas operacionesLa situación con el anuncio de nuevas operaciones es aún más complicada. Incluir la posibilidad de tal declaración en el idioma no es difícil, pero su implementación está plagada de dificultades significativas. Declarar una nueva operación es, de hecho, crear una nueva palabra clave de lenguaje de programación, complicada por el hecho de que las operaciones en el texto, como regla, pueden seguir sin separadores con otros tokens. Cuando aparecen, surgen dificultades adicionales en la organización del analizador léxico. Por ejemplo, si el idioma ya tiene las operaciones “+” y el unario “-” (cambio de signo), entonces la expresión a+-bse puede interpretar con precisión como a + (-b), pero si se declara una nueva operación en el programa +-, surge inmediatamente la ambigüedad, porque el misma expresión ya se puede analizar y cómo a (+-) b. El desarrollador e implementador del lenguaje debe lidiar con estos problemas de alguna manera. Las opciones, de nuevo, pueden ser diferentes: exigir que todas las operaciones nuevas sean de un solo carácter, postular que en caso de discrepancias, se elige la versión “más larga” de la operación (es decir, hasta el siguiente conjunto de caracteres leído por el traductor coincide con cualquier operación, se sigue leyendo), intenta detectar colisiones durante la traducción y generar errores en casos controvertidos... De una forma u otra, los lenguajes que permiten la declaración de nuevas operaciones solucionan estos problemas.
No hay que olvidar que para las nuevas operaciones también está el tema de determinar la asociatividad y la prioridad. Ya no existe una solución prefabricada en forma de una operación de lenguaje estándar y, por lo general, solo tiene que configurar estos parámetros con las reglas del idioma. Por ejemplo, haga que todas las operaciones nuevas sean asociativas a la izquierda y déles la misma prioridad fija, o introduzca en el lenguaje los medios para especificar ambas.
Cuando se usan operadores, funciones y procedimientos sobrecargados en lenguajes fuertemente tipados, donde cada variable tiene un tipo predeclarado, depende del compilador decidir qué versión del operador sobrecargado usar en cada caso particular, sin importar cuán complejo sea. . Esto significa que para los lenguajes compilados, el uso de la sobrecarga de operadores no reduce el rendimiento de ninguna manera; en cualquier caso, hay una llamada de función o operación bien definida en el código objeto del programa. La situación es diferente cuando es posible usar variables polimórficas en el lenguaje, variables que pueden contener valores de diferentes tipos en diferentes momentos.
Dado que el tipo de valor al que se aplicará la operación sobrecargada se desconoce en el momento de la traducción del código, el compilador no tiene la oportunidad de elegir la opción deseada por adelantado. En esta situación, se ve obligado a incrustar un fragmento en el código objeto que, inmediatamente antes de realizar esta operación, determinará los tipos de los valores en los argumentos y seleccionará dinámicamente una variante correspondiente a este conjunto de tipos. Además, tal definición debe hacerse cada vez que se realiza la operación, porque incluso el mismo código, al ser llamado por segunda vez, bien puede ejecutarse de manera diferente ...
Por lo tanto, el uso de la sobrecarga de operadores en combinación con variables polimórficas hace que sea inevitable determinar dinámicamente a qué código llamar.
El uso de la sobrecarga no es considerado una bendición por todos los expertos. Si la sobrecarga de funciones y procedimientos, en general, no encuentra serias objeciones (en parte porque no conduce a algunos de los problemas típicos de "operadores", en parte porque es menos tentador hacer un mal uso de ellos), entonces la sobrecarga de operadores, como en principio, y en particular implementaciones del lenguaje, está sujeto a críticas bastante severas por parte de muchos teóricos y profesionales de la programación.
Los críticos señalan que los problemas de identificación, precedencia y asociatividad descritos anteriormente a menudo hacen que lidiar con operadores sobrecargados sea innecesariamente difícil o poco natural:
Cuánto puede compensar la conveniencia de utilizar sus propias operaciones el inconveniente de deteriorar la capacidad de control del programa es una pregunta que no tiene una respuesta clara.
Algunos críticos se pronuncian en contra de las operaciones de sobrecarga, basándose en los principios generales de la teoría del desarrollo de software y la práctica industrial real.
Este problema se deriva naturalmente de los dos anteriores. Se nivela fácilmente por la aceptación de acuerdos y la cultura general de programación.
La siguiente es una clasificación de algunos lenguajes de programación según si permiten la sobrecarga de operadores y si los operadores están limitados a un conjunto predefinido:
muchos operadores |
Sin sobrecarga |
hay una sobrecarga |
---|---|---|
Solo predefinido |
Ada | |
Es posible introducir nuevos |
Algol 68 |