El comportamiento de cortocircuito de los operadores &&
y ||
es una herramienta increíble para los programadores.
Pero, ¿por qué pierden este comportamiento cuando se sobrecargan? Entiendo que los operadores son simplemente azúcar sintáctica para las funciones, pero los operadores bool
tienen este comportamiento, ¿por qué debería restringirse a este tipo único? ¿Hay algún razonamiento técnico detrás de esto?
operator&&(const Foo& lhs, const Foo& rhs) : (lhs.bars == 0)
{true, false, nil}
. Yanil&& x == nil
que podría cortocircuitar.std::valarray<bool> a, b, c;
, ¿cómo te imaginasa || b || c
estar en cortocircuito?operator&&
ooperator||
depende de ambos operandos que se evalúan. Mantener la compatibilidad con versiones anteriores es (o debería ser) importante al agregar funciones a un idioma existente.Respuestas:
Todos los procesos de diseño resultan en compromisos entre objetivos mutuamente incompatibles. Desafortunadamente, el proceso de diseño para el
&&
operador sobrecargado en C ++ produjo un resultado final confuso: que&&
se omite la característica que desea , su comportamiento de cortocircuito.Los detalles de cómo ese proceso de diseño terminó en este desafortunado lugar, aquellos que no conozco. Sin embargo, es relevante ver cómo un proceso de diseño posterior tuvo en cuenta este resultado desagradable. En C #, el
&&
operador sobrecargado está en cortocircuito. ¿Cómo lograron eso los diseñadores de C #?Una de las otras respuestas sugiere "levantamiento lambda". Es decir:
podría realizarse como algo moralmente equivalente a:
donde el segundo argumento usa algún mecanismo para la evaluación diferida, de modo que cuando se evalúa, se producen los efectos secundarios y el valor de la expresión. La implementación del operador sobrecargado solo haría la evaluación diferida cuando sea necesario.
Esto no es lo que hizo el equipo de diseño de C #. (Aparte: aunque el levantamiento de lambda es lo que hice cuando llegó el momento de hacer una representación del
??
operador del árbol de expresión , lo que requiere que ciertas operaciones de conversión se realicen con pereza. Sin embargo, describir eso en detalle sería una digresión importante. Basta decir: levantamiento de lambda funciona pero es lo suficientemente pesado como para desear evitarlo).Por el contrario, la solución C # divide el problema en dos problemas separados:
Por lo tanto, el problema se resuelve al hacer ilegal la sobrecarga
&&
directa. Por el contrario, en C # debe sobrecargar dos operadores, cada uno de los cuales responde a una de esas dos preguntas.(Aparte: en realidad, tres. C # requiere que si
false
se proporciona el operadortrue
, también se debe proporcionar el operador , lo que responde a la pregunta: ¿esto es "verdadero-ish?" requiere ambos.)Considere una declaración de la forma:
El compilador genera código para esto como creía que había escrito este pseudo-C #:
Como puede ver, el lado izquierdo siempre se evalúa. Si se determina que es "falso-ish", entonces es el resultado. De lo contrario, se evalúa el lado derecho y se invoca al operador ansioso definido por el usuario
&
.El
||
operador se define de manera análoga, como una invocación del operador verdadero y el|
operador ansioso :Mediante la definición de los cuatro operadores -
true
,false
,&
y|
- C # le permite no sólo de hablarcleft && cright
, sino también no cortocircuitoscleft & cright
, y tambiénif (cleft) if (cright) ...
, yc ? consequence : alternative
, ywhile(c)
, y así sucesivamente.Ahora, dije que todos los procesos de diseño son el resultado de un compromiso. Aquí los diseñadores del lenguaje C # consiguieron un cortocircuito
&&
y la||
derecha, pero hacerlo requiere una sobrecarga cuatro operadores en lugar de dos , que algunas personas encuentran problemas. La función verdadero / falso del operador es una de las características menos conocidas de C #. El objetivo de tener un lenguaje sensible y directo que sea familiar para los usuarios de C ++ se opuso a los deseos de tener un corto circuito y el deseo de no implementar el levantamiento lambda u otras formas de evaluación perezosa. Creo que fue una posición de compromiso razonable, pero es importante darse cuenta de que es una posición de compromiso. Solo un diferente posición de compromiso que los diseñadores de C ++ aterrizaron.Si el tema del diseño del lenguaje para tales operadores le interesa, considere leer mi serie sobre por qué C # no define estos operadores en booleanos que aceptan valores NULL:
http://ericlippert.com/2012/03/26/null-is-not-false-part-one/
fuente
your post
sea irrelevante.His noticing your distinct writing style
es irrelevante.bool
, puede usar&&
y||
sin implementaroperator true/false
ooperator &/|
en C # no hay problema. El problema surge precisamente en la situación en la que no hay conversiónbool
posible o donde no se desea.El punto es que (dentro de los límites de C ++ 98) el operando de la derecha se pasaría a la función del operador sobrecargado como argumento. Al hacerlo, ya sería evaluado . No hay nada que el
operator||()
o eloperator&&()
código pueda o no pueda hacer que evite esto.El operador original es diferente, porque no es una función, sino que se implementa en un nivel inferior del lenguaje.
Las características adicionales del lenguaje podrían haber hecho que la no evaluación del operando de la mano derecha sea sintácticamente posible . Sin embargo, no se molestaron porque solo hay unos pocos casos selectos en los que esto sería semánticamente útil. (Al igual
? :
que, que no está disponible para sobrecargar en absoluto.(Les tomó 16 años lograr que las lambdas ingresen al estándar ...)
En cuanto al uso semántico, considere:
Esto se reduce a:
Piense qué es exactamente lo que le gustaría hacer con el objeto B (de tipo desconocido) aquí, aparte de llamar a un operador de conversión
bool
, y cómo lo pondría en palabras para la definición del lenguaje.Y si usted está llamando a la conversión a bool, así ...
hace lo mismo, ahora lo hace? Entonces, ¿por qué sobrecargar en primer lugar?
fuente
export
.)bool
operador de conversión para cualquiera de las clases también tiene acceso a todas las variables miembro y funciona bien con el operador incorporado. ¡Cualquier otra cosa que no sea conversión a bool no tiene sentido semántico para la evaluación de cortocircuito de todos modos! Trate de abordar esto desde un punto de vista semántico, no sintáctico: ¿Qué intentaría lograr, no cómo lo haría?&
y&&
no son el mismo operador. Gracias por ayudarme a darme cuenta de eso.if (x != NULL && x->foo)
requiere cortocircuito, no por velocidad, sino por seguridad.Una característica tiene que ser pensada, diseñada, implementada, documentada y enviada.
Ahora que lo pensamos, veamos por qué podría ser fácil ahora (y difícil de hacer entonces). También tenga en cuenta que solo hay una cantidad limitada de recursos, por lo que agregarlo podría haber cortado algo más (¿qué le gustaría renunciar a él?).
En teoría, todos los operadores podrían permitir el comportamiento de cortocircuito con solo una característica de lenguaje adicional "menor" , a partir de C ++ 11 (cuando se introdujeron las lambdas, 32 años después de que comenzara "C con clases" en 1979, un 16 todavía respetable después de c ++ 98):
C ++ solo necesitaría una forma de anotar un argumento como evaluado de forma diferida, una lambda oculta, para evitar la evaluación hasta que sea necesario y permitido (se cumplen las condiciones previas).
¿Cómo sería esa característica teórica (recuerde que cualquier característica nueva debería ser ampliamente utilizable)?
Una anotación
lazy
, que se aplica a un argumento de función, convierte la función en una plantilla que espera un functor, y hace que el compilador empaquete la expresión en un functor:Se vería debajo de la cubierta como:
Tenga en cuenta que la lambda permanece oculta y se llamará a lo sumo una vez.
No debería haber degradación del rendimiento debido a esto, aparte de la reducción de las posibilidades de eliminación de subexpresiones comunes.
Además de la complejidad de implementación y la complejidad conceptual (cada característica aumenta ambas, a menos que alivie lo suficiente esas complejidades para otras características), veamos otra consideración importante: la compatibilidad con versiones anteriores.
Si bien esta función de lenguaje no rompería ningún código, cambiaría sutilmente cualquier API aprovechándola, lo que significa que cualquier uso en las bibliotecas existentes sería un cambio silencioso.
Por cierto: esta característica, aunque es más fácil de usar, es estrictamente más fuerte que la solución C # de división
&&
y||
en dos funciones cada una para una definición separada.fuente
&&
para tomar un argumento de tipo "puntero para funcionar devolviendo T" y una regla de conversión adicional que permite que una expresión de argumento de tipo T se convierta implícitamente en una expresión lambda. Tenga en cuenta que esta no es una conversión ordinaria, ya que debe hacerse a nivel sintáctico: convertir en tiempo de ejecución un valor de tipo T en una función no sería útil ya que la evaluación ya se habría hecho.Con racionalización retrospectiva, principalmente porque
Para garantizar un cortocircuito garantizado (sin introducir una nueva sintaxis), los operadores tendrían que estar restringidos a
resultadosprimer argumento real convertible abool
, yel cortocircuito se puede expresar fácilmente de otras formas, cuando sea necesario.
Por ejemplo, si una clase
T
tiene asociados&&
y||
operadores, entonces la expresióndonde
a
,b
yc
son expresiones de tipoT
, se pueden expresar con cortocircuito comoo quizás más claramente como
La aparente redundancia conserva los efectos secundarios de las invocaciones del operador.
Si bien la reescritura lambda es más detallada, su mejor encapsulación permite definir dichos operadores.
No estoy completamente seguro de la conformidad estándar de todo lo siguiente (todavía un poco de influencia), pero se compila limpiamente con Visual C ++ 12.0 (2013) y MinGW g ++ 4.8.2:
Salida:
Aquí cada
!!
bang-bang muestra una conversión abool
, es decir, una comprobación de valor de argumento.Dado que un compilador puede hacer lo mismo fácilmente y, además, optimizarlo, esta es una posible implementación demostrada y cualquier reclamo de imposibilidad debe clasificarse en la misma categoría que los reclamos de imposibilidad en general, es decir, generalmente bollocks.
fuente
&&
- necesitaría una línea adicional comoif (!a) { return some_false_ish_T(); }
- y a tu primera viñeta: el cortocircuito se trata de los parámetros convertibles a bool, no de los resultados.bool
es necesaria para hacer un cortocircuito.||
pero no el&&
. El otro comentario estaba dirigido a "tendría que estar restringido a resultados convertibles a bool" en su primer punto de viñeta - debería leer "restringido a parámetros convertibles a bool" imo.bool
para verificar el cortocircuito de otros operadores en la expresión. Como, el resultado dea && b
tiene que convertirse abool
para comprobar el cortocircuito del OR lógico ena && b || c
.tl; dr : no vale la pena el esfuerzo, debido a la demanda muy baja (¿quién usaría la función?) en comparación con los costos bastante altos (se necesita una sintaxis especial).
Lo primero que viene a la mente es que la sobrecarga del operador es solo una forma elegante de escribir funciones, mientras que la versión booleana de los operadores
||
y&&
son cosas de buitlin. Eso significa que el compilador tiene la libertad de cortocircuitarlos, mientras que la expresiónx = y && z
con no booleanoy
yz
tiene que conducir a una llamada a una función comoX operator&& (Y, Z)
. Esto significaría quey && z
es solo una forma elegante de escribir,operator&&(y,z)
que es solo una llamada de una función con un nombre extraño donde ambos parámetros deben evaluarse antes de llamar a la función (incluido cualquier cosa que considere un cortocircuito apropiado).Sin embargo, se podría argumentar que debería ser posible hacer que la traducción de
&&
operadores sea algo más sofisticada, como lo es para elnew
operador que se traduce en llamar a la funciónoperator new
seguida de una llamada de constructor.Técnicamente, esto no sería un problema, habría que definir una sintaxis de lenguaje específica para la condición previa que permita el cortocircuito. Sin embargo, el uso de cortocircuitos se restringiría a los casos en los que
Y
sea convertibleX
o, de lo contrario, tenía que haber información adicional sobre cómo hacer realmente el cortocircuito (es decir, calcular el resultado solo desde el primer parámetro). El resultado tendría que verse más o menos así:Rara vez se quiere sobrecargar
operator||
yoperator&&
, porque rara vez hay un caso en el que la escrituraa && b
es realmente intuitiva en un contexto no booleano. Las únicas excepciones que conozco son las plantillas de expresión, por ejemplo, para DSL incrustadas. Y solo unos pocos de esos pocos casos se beneficiarían de la evaluación de cortocircuito. Las plantillas de expresión generalmente no, porque se usan para formar árboles de expresión que se evalúan más adelante, por lo que siempre necesita ambos lados de la expresión.En resumen: ni los escritores de compiladores ni los autores de estándares sintieron la necesidad de saltar a través de aros y definir e implementar una sintaxis engorrosa adicional, solo porque uno en un millón podría tener la idea de que sería bueno tener un cortocircuito definido por el usuario
operator&&
yoperator||
, simplemente para llegar a la conclusión de que no es menos esfuerzo que escribir la lógica a mano.fuente
lazy
que convierten la expresión dada como argumentos implícitamente en una función anónima. Esto le da a la función llamada la opción de llamar a ese argumento, o no. Entonces, si el idioma ya tiene lambdas, la sintaxis adicional necesaria es muy pequeña. "Pseudocódigo": X y (A a, perezoso B b) {if (cond (a)) {return short (a); } else {actual (a, b ()); }}std::function<B()>
, que incurriría en una cierta sobrecarga. O si está dispuesto a alinearlo, hágalotemplate <class F> X and(A a, F&& f){ ... actual(a,F()) ...}
. Y tal vez sobrecargarlo con elB
parámetro "normal" , para que la persona que llama pueda decidir qué versión elegir. Lalazy
sintaxis puede ser más conveniente pero tiene una cierta compensación de rendimiento.std::function
versuslazy
es que el primero se puede evaluar varias veces. Un parámetro diferidofoo
que se usa comofoo+foo
todavía solo se evalúa una vez.X
se puede calcularY
solo. Muy diferente.std::ostream& operator||(char* a, lazy char*b) {if (a) return std::cout<<a;return std::cout<<b;}
. A menos que esté utilizando un uso muy informal de "conversión".operator&&
. La pregunta no es si es posible, sino por qué no hay una manera conveniente y corta.Lambdas no es la única forma de introducir la pereza. La evaluación diferida es relativamente sencilla utilizando plantillas de expresión en C ++. No hay necesidad de palabras clave
lazy
y se puede implementar en C ++ 98. Los árboles de expresión ya se mencionan anteriormente. Las plantillas de expresión son árboles de expresión del hombre pobre (pero inteligente). El truco consiste en convertir la expresión en un árbol de instancias recursivamente anidadas de laExpr
plantilla. El árbol se evalúa por separado después de la construcción.Los siguientes implementos de código corto-circuito
&&
y||
operadores para la claseS
siempre que proporcionalogical_and
ylogical_or
funciones libres y que se puede convertir enbool
. El código está en C ++ 14 pero la idea también es aplicable en C ++ 98. Ver ejemplo en vivo .fuente
Se permite el cortocircuito de los operadores lógicos porque es una "optimización" en la evaluación de las tablas de verdad asociadas. Es una función de la lógica misma, y esta lógica está definida.
Los operadores lógicos sobrecargados personalizados no están obligados a seguir la lógica de estas tablas de verdad.
Por lo tanto, toda la función debe evaluarse según lo normal. El compilador debe tratarlo como un operador (o función) sobrecargado normal y aún puede aplicar optimizaciones como lo haría con cualquier otra función.
La gente sobrecarga los operadores lógicos por una variedad de razones. Por ejemplo; pueden tener un significado específico en un dominio específico que no es el lógico "normal" al que la gente está acostumbrada.
fuente
El cortocircuito se debe a la tabla de verdad de "y" y "o". ¿Cómo sabría qué operación va a definir el usuario y cómo sabe que no tendrá que evaluar al segundo operador?
fuente
: (<condition>)
después de la declaración del operador para especificar una condición en la que no se evalúa el segundo argumento?Solo quiero responder esta parte. La razón es que el incorporado
&&
y las||
expresiones no se implementan con funciones como lo están los operadores sobrecargados.Tener la lógica de cortocircuito integrada en la comprensión del compilador de expresiones específicas es fácil. Es como cualquier otro flujo de control incorporado.
Pero la sobrecarga del operador se implementa con funciones en su lugar, que tienen reglas particulares, una de las cuales es que todas las expresiones utilizadas como argumentos se evalúan antes de llamar a la función. Obviamente, se podrían definir reglas diferentes, pero ese es un trabajo más grande.
fuente
&&
,||
y,
se debe permitir? El hecho de que C ++ no tenga un mecanismo para permitir que las sobrecargas se comporten como algo diferente a las llamadas a funciones explica por qué las sobrecargas de esas funciones no pueden hacer otra cosa, pero no explica por qué esos operadores son sobrecargables en primer lugar. Sospecho que la verdadera razón es simplemente que fueron incluidos en una lista de operadores sin pensarlo mucho.