Acabo de leer uno de los artículos de Joel en el que dice:
En general, tengo que admitir que tengo un poco de miedo a las características del lenguaje que ocultan cosas . Cuando veas el código
i = j * 5;
... en C sabes, al menos, que j se multiplica por cinco y los resultados se almacenan en i.
Pero si ve el mismo fragmento de código en C ++, no sabe nada. Nada. La única forma de saber lo que realmente está sucediendo en C ++ es averiguar qué tipos son i y j, algo que podría declararse en otro lugar. Esto se debe a que j puede ser de un tipo que se ha
operator*
sobrecargado y hace algo terriblemente ingenioso cuando intentas multiplicarlo.
(El énfasis es mío). ¿Te asustan las características del lenguaje que ocultan cosas? ¿Cómo puedes tener miedo de eso? ¿No es ocultar cosas (también conocido como abstracción ) una de las ideas clave de la programación orientada a objetos? Cada vez que llama a un método a.foo(b)
, no tiene idea de lo que podría hacer. Tienes que averiguar qué tipos a
y qué b
son, algo que podría declararse en otro lugar. Entonces, ¿deberíamos eliminar la programación orientada a objetos, porque oculta demasiadas cosas del programador?
¿Y en qué se j * 5
diferencia de j.multiply(5)
lo que podría tener que escribir en un idioma que no sea compatible con la sobrecarga del operador? Una vez más, tendrías que averiguar el tipo j
y echar un vistazo dentro del multiply
método, porque he aquí, j
podría ser de un tipo que tiene un multiply
método que hace algo terriblemente ingenioso.
"Muahaha, soy un programador malvado que nombra un método multiply
, pero lo que realmente hace es totalmente oscuro y no intuitivo y no tiene absolutamente nada que ver con multiplicar las cosas". ¿Es ese un escenario que debemos tener en cuenta al diseñar un lenguaje de programación? ¡Entonces tenemos que abandonar los identificadores de los lenguajes de programación porque pueden ser engañosos!
Si desea saber qué hace un método, puede echar un vistazo a la documentación o echar un vistazo dentro de la implementación. La sobrecarga del operador es solo azúcar sintáctica, y no veo cómo cambia el juego en absoluto.
Por favor iluminame.
Respuestas:
La abstracción 'oculta' el código para que no tenga que preocuparse por el funcionamiento interno y, a menudo, para que no pueda cambiarlo, pero la intención no era evitar que lo mirara. Simplemente hacemos suposiciones sobre los operadores y, como dijo Joel, podría estar en cualquier lugar. Tener una función de programación que requiera que todos los operadores sobrecargados se establezcan en una ubicación específica puede ayudar a encontrarlo, pero no estoy seguro de que facilite su uso.
No veo que hacer * haga algo que no se parezca mucho a la multiplicación mejor que una función llamada Get_Some_Data que elimina datos.
fuente
<<
operador definido en secuencias que no tiene nada que ver con el desplazamiento a nivel de bits, directamente en la biblioteca estándar de C ++.En mi humilde opinión, las características del lenguaje, como la sobrecarga del operador, le dan al programador más potencia. Y, como todos sabemos, un gran poder conlleva una gran responsabilidad. Las características que le dan más poder también le brindan más formas de dispararse en el pie y, obviamente, deben usarse con prudencia.
Por ejemplo, tiene mucho sentido sobrecargar el
+
o el*
operador paraclass Matrix
oclass Complex
. Todos sabrán instantáneamente lo que significa. Por otro lado, para mí el hecho de que eso+
significa concatenación de cadenas no es del todo obvio, a pesar de que Java hace esto como parte del lenguaje, y STL lo hace porstd::string
usar la sobrecarga del operador.Otro buen ejemplo de cuándo la sobrecarga del operador hace que el código sea más claro son los punteros inteligentes en C ++. Desea que los punteros inteligentes se comporten como punteros regulares tanto como sea posible, por lo que tiene mucho sentido sobrecargar a los operadores
*
y al unario->
.En esencia, la sobrecarga del operador no es más que otra forma de nombrar una función. Y hay una regla para nombrar funciones: el nombre debe ser descriptivo, lo que hace que sea inmediatamente obvio lo que hace la función. La misma regla exacta se aplica a la sobrecarga del operador.
fuente
*
de esos puede causar confusión. Podría decirse que podría usarlooperator*()
para el producto punto yoperator%()
para el producto cruzado, pero no lo haría para una biblioteca de uso general.A-B
comoB-A
tampoco, y todos los operadores sigue ese patrón. Aunque siempre hay una excepción: cuando el compilador puede demostrar que no importa, se le permite reorganizar todo.En Haskell "+", "-", "*", "/" etc. son solo funciones (infijo).
¿Debería nombrar una función infija "más" como en "4 más 2"? Por qué no, si además es lo que hace su función. ¿Debería nombrar su función "más" "+"? Por qué no.
Creo que el problema con los llamados "operadores" es que se parecen principalmente a las operaciones matemáticas y no hay muchas maneras de interpretarlas y, por lo tanto, hay altas expectativas sobre lo que hace un método / función / operador.
EDITAR: dejó mi punto más claro
fuente
int
,float
,long long
y lo que sea. Entonces, ¿de qué se trata todo eso?+
para diferentes tipos de números incorporados, sino a la creación de sobrecargas definidas por el usuario. De ahí mi comentario.Basado en las otras respuestas que he visto, solo puedo concluir que la verdadera objeción a la sobrecarga del operador es el deseo de un código obvio de inmediato.
Esto es trágico por dos razones:
fuente
Estoy un poco de acuerdo.
Si escribe
multiply(j,5)
,j
podría ser de tipo escalar o matricial, haciendomultiply()
más o menos complejo, dependiendo de lo quej
sea. Sin embargo, si abandona la idea de sobrecargar por completo, entonces la función tendría que nombrarsemultiply_scalar()
omultiply_matrix()
lo que haría obvio lo que está sucediendo debajo.Hay un código donde muchos de nosotros lo preferiríamos de una manera y hay un código donde la mayoría de nosotros lo preferiría de la otra manera. La mayor parte del código, sin embargo, cae en el punto medio entre esos dos extremos. Lo que prefiera allí depende de sus antecedentes y preferencias personales.
fuente
multiply_matrix()
no les gustará tampoco la programación genérica.real_multiply()
o menos. Los desarrolladores a menudo no son buenos con los nombres, yoperator*()
al menos va a ser consistente.operator*()
podría hacer algo estúpido,j
es una macro que evalúa expresiones que involucran cinco llamadas a funciones, y otras cosas. Entonces ya no puedes comparar los dos enfoques. Pero sí, nombrar bien las cosas es difícil, aunque vale la pena el tiempo que sea necesario.Veo dos problemas con la sobrecarga del operador.
&&
,||
o,
, se pierden los puntos de secuencia que están implícitos en las variantes incorporadas de estos operadores (así como el comportamiento de los cortocircuitos de los operadores lógicos). Por esta razón, es mejor no sobrecargar estos operadores, incluso si el idioma lo permite.operator<<
para las transmisiones.fuente
&&
y||
de una manera que no implique secuenciación fue un gran error (en mi humilde opinión, si C ++ iba a permitir la sobrecarga de esos, debería haber utilizado un formato especial de "dos funciones", con la primera función requerida para devolver un tipo que era convertible implícitamente a un número entero; la segunda función podría tomar dos o tres argumentos, siendo el argumento "extra" de la segunda función el tipo de retorno de la primera. El compilador llamará a la primera función y luego, si devuelve un valor distinto de cero, evalúe el segundo operando y llame a la segunda función).foo.bar[3].X
sea manejada porfoo
la clase, en lugar de requerirfoo
exponer un miembro que podría admitir la suscripción y luego exponer a un miembroX
. Si uno quisiera forzar la evaluación a través del acceso real de un miembro, uno escribiría((foo.bar)[3]).X
.Según mi experiencia personal, la forma en Java de permitir múltiples métodos, pero no sobrecargar al operador, significa que cada vez que ve a un operador sabe exactamente lo que hace.
No tiene que ver si
*
invoca un código extraño, pero sepa que es una multiplicación y que se comporta exactamente como se define en la Especificación del lenguaje Java. Esto significa que puede concentrarse en el comportamiento real en lugar de descubrir todas las cosas de wicket definidas por el programador.En otras palabras, prohibir la sobrecarga del operador es un beneficio para el lector , no para el escritor , y por lo tanto, hace que los programas sean más fáciles de mantener.
fuente
list.get(n)
sintaxis?std::list
no se sobrecargaoperator[]
(ni proporciona ningún otro medio de indexación en la lista), porque dicha operación sería O (n), y una interfaz de lista no debería exponer dicha función si le importa la eficiencia. Los clientes pueden verse tentados a iterar sobre listas vinculadas con índices, haciendo innecesariamente los algoritmos O (n) O (n ^ 2). Esto se ve con bastante frecuencia en el código Java, especialmente si las personas trabajan con laList
interfaz que tiene como objetivo eliminar la complejidad por completo.time.add(anotherTime)
, también tendrá que verificar si el programador de la biblioteca implementó la operación de agregar "correctamente" (lo que sea que eso signifique).Una diferencia entre sobrecargar
a * b
y llamarmultiply(a,b)
es que esta última puede ser fácilmente buscada. Si lamultiply
función no está sobrecargada para diferentes tipos, puede averiguar exactamente qué va a hacer la función, sin tener que rastrear los tipos dea
yb
.Linus Torvalds tiene un argumento interesante sobre la sobrecarga del operador. En algo como el desarrollo del kernel de Linux, donde la mayoría de los cambios se envían a través de parches por correo electrónico, es importante que los encargados del mantenimiento puedan comprender qué hará un parche con solo unas pocas líneas de contexto alrededor de cada cambio. Si las funciones y los operadores no están sobrecargados, entonces el parche se puede leer más fácilmente de forma independiente del contexto, ya que no tiene que revisar el archivo modificado para determinar cuáles son todos los tipos y verificar si hay operadores sobrecargados.
fuente
Sospecho que tiene algo que ver con romper las expectativas. Estoy acostumbrado a C ++, estás acostumbrado a que el comportamiento del operador no esté completamente dictado por el lenguaje, y no te sorprenderás cuando un operador hace algo extraño. Si está acostumbrado a los idiomas que no tienen esa función, y luego ve el código C ++, trae consigo las expectativas de esos otros idiomas, y puede sorprenderse cuando descubre que un operador sobrecargado hace algo raro.
Personalmente creo que hay una diferencia. Cuando puede cambiar el comportamiento de la sintaxis incorporada del lenguaje, se vuelve más opaco para razonar. Los lenguajes que no permiten la metaprogramación son sintácticamente menos potentes, pero conceptualmente más fáciles de entender.
fuente
Creo que la sobrecarga de operadores matemáticos no es el problema real con la sobrecarga de operadores en C ++. Creo que sobrecargar operadores que no deberían depender del contexto de la expresión (es decir, tipo) es "malvado". Por ejemplo, sobrecarga
,
[ ]
( )
->
->*
new
delete
o incluso el unario*
. Tienes un cierto conjunto de expectativas de esos operadores que nunca deberían cambiar.fuente
operator[]
, functors sinoperator()
, punteros inteligentes sinoperator->
y así sucesivamente.[]
siempre debe ser un descriptor de acceso tipo matriz y->
siempre debe significar acceder a un miembro. No importa si es realmente una matriz o un contenedor diferente, o si es un puntero inteligente o no.Entiendo perfectamente que no te gusta el argumento de Joel sobre esconderse. Yo tampoco. De hecho, es mucho mejor usar '+' para cosas como los tipos numéricos incorporados o para los propios, como, por ejemplo, matriz. Admito que esto es ordenado y elegante para poder multiplicar dos matrices con '*' en lugar de '.multiply ()'. Y después de todo, tenemos el mismo tipo de abstracción en ambos casos.
Lo que duele aquí es la legibilidad de su código. En casos de la vida real, no en el ejemplo académico de la multiplicación de matrices. Especialmente si su idioma permite definir operadores que no están inicialmente presentes en el núcleo del idioma, por ejemplo
=:=
. En este punto surgen muchas preguntas adicionales. ¿De qué se trata ese maldito operador? Quiero decir, ¿cuál es la precedencia de esa cosa? ¿Qué es la asociatividad? ¿En qué orden sea =:= b =:= c
ejecuta realmente?Eso ya es un argumento contra la sobrecarga del operador. ¿Todavía no está convencido? ¿Comprobar las reglas de precedencia no te llevó más de 10 segundos? Ok, vamos más allá.
Si comienza a usar un lenguaje que permite la sobrecarga de operadores, por ejemplo, el popular cuyo nombre comienza con 'S', aprenderá rápidamente que a los diseñadores de bibliotecas les encanta anular operadores. Por supuesto, están bien educados, siguen las mejores prácticas (sin cinismo aquí) y todas sus API tienen mucho sentido cuando las miramos por separado.
Ahora imagine que tiene que usar algunas API que hacen un uso intensivo de los operadores que se sobrecargan en una sola pieza de código. O incluso mejor: tienes que leer un código heredado como ese. Esto es cuando la sobrecarga del operador realmente apesta. Básicamente, si hay muchos operadores sobrecargados en un lugar, pronto comenzarán a mezclarse con los otros caracteres no alfanuméricos en el código de su programa. Se mezclarán con caracteres no alfanuméricos que no son realmente operadores, sino algunos elementos de gramática del lenguaje más fundamentales que definen cosas como bloques y ámbitos, declaraciones de control de flujo de forma o denotan algunas cosas meta. Tendrá que ponerse las gafas y acercar los ojos 10 cm a la pantalla LCD para comprender ese desorden visual.
fuente
En general, evito usar la sobrecarga del operador de manera no intuitiva. Es decir, si tengo una clase numérica, la sobrecarga * es aceptable (y se recomienda). Sin embargo, si tengo un empleado de clase, ¿qué haría la sobrecarga *? En otras palabras, sobrecargue a los operadores de formas intuitivas que faciliten su lectura y comprensión.
Aceptable / Animado:
Inaceptable:
fuente
Además de lo que ya se ha dicho aquí, hay un argumento más en contra de la sobrecarga del operador. De hecho, si escribe
+
, es obvio que quiere decir la adición de algo a algo. Pero este no es siempre el caso.C ++ en sí mismo proporciona un gran ejemplo de tal caso. ¿Cómo se
stream << 1
supone que debe leerse? corriente desplazada a la izquierda por 1? No es obvio en absoluto a menos que sepa explícitamente que << en C ++ también escribe en la secuencia. Sin embargo, si esta operación se implementara como un método, ningún desarrollador sensato escribiríao.leftShift(1)
, sería algo asío.write(1)
.La conclusión es que al hacer que la sobrecarga del operador no esté disponible, el lenguaje hace que los programadores piensen en los nombres de las operaciones. Incluso si el nombre elegido no es perfecto, aún es más difícil interpretar mal un nombre que un signo.
fuente
En comparación con los métodos detallados, los operadores son más cortos, pero tampoco requieren paréntesis. Los paréntesis son relativamente inconvenientes para escribir. Y debes equilibrarlos. En total, cualquier llamada al método requiere tres caracteres de ruido simple en comparación con un operador. Esto hace que el uso de operadores sea muy, muy tentador.
¿Por qué si alguien querría esto
cout << "Hello world"
?El problema con la sobrecarga es que la mayoría de los programadores son increíblemente flojos y la mayoría de los programadores no pueden permitirse el lujo de serlo.
Lo que lleva a los programadores de C ++ al abuso de la sobrecarga del operador no es su presencia, sino la ausencia de una forma más ordenada de realizar llamadas a métodos. Y las personas no solo tienen miedo de la sobrecarga del operador porque es posible, sino porque ya está hecho.
Tenga en cuenta que, por ejemplo, en Ruby y Scala, nadie tiene miedo de la sobrecarga del operador. Además del hecho de que el uso de operadores no es realmente más corto que los métodos, otra razón es que Ruby limita la sobrecarga de operadores a un mínimo razonable, mientras que Scala le permite declarar sus propios operadores, lo que hace que evitar colisiones sea trivial.
fuente
La razón por la que la sobrecarga de operadores es aterradora es porque hay una gran cantidad de programadores que ni siquiera PENSARÍAN que
*
no significa simplemente "multiplicar", mientras que un método comofoo.multiply(bar)
al menos instantáneamente señala a ese programador que alguien escribió un método de multiplicación personalizado . En ese momento se preguntarían por qué e investigarían.He trabajado con "buenos programadores" que estaban en posiciones de alto nivel que crearían métodos llamados "CompareValues" que tomarían 2 argumentos, y aplicarían los valores de uno a otro y devolverían un valor booleano. O un método llamado "LoadTheValues" que iría a la base de datos para otros 3 objetos, obtendría valores, haría cálculos, lo modificaría
this
y lo guardaría en la base de datos.Si estoy trabajando en un equipo con ese tipo de programadores, instantáneamente sé investigar en qué han trabajado. Si sobrecargan a un operador, no tengo forma de saber que lo hicieron, excepto asumir que lo hicieron e ir a buscar.
En un mundo perfecto, o en un equipo con programadores perfectos, la sobrecarga del operador es probablemente una herramienta fantástica. Sin embargo, todavía tengo que trabajar en un equipo de programadores perfectos, por eso da miedo.
fuente