¿Son obligatorios los operadores lógicos de cortocircuito? Y orden de evaluación?

140

¿El estándar ANSI mandato de los operadores lógicos que se produzca un cortocircuito, ya sea en C o C ++?

Estoy confundido porque recuerdo el libro de K&R que dice que su código no debería depender de que estas operaciones se cortocircuiten, ya que es posible que no lo hagan. ¿Podría alguien indicar dónde en el estándar se dice que las operaciones lógicas siempre están en cortocircuito? Estoy principalmente interesado en C ++, una respuesta también para C sería genial.

También recuerdo haber leído (no recuerdo dónde) que el orden de evaluación no está estrictamente definido, por lo que su código no debería depender o asumir que las funciones dentro de una expresión se ejecutarían en un orden específico: al final de una declaración, todas las funciones referenciadas habrá sido llamado, pero el compilador tiene libertad para seleccionar el orden más eficiente.

¿El estándar indica el orden de evaluación de esta expresión?

if( functionA() && functionB() && functionC() ) cout<<"Hello world";
Joe Pineda
fuente
12
Cuidado: es cierto para los tipos de POD. Pero si sobrecarga el operador && u operador || para una clase particular, estos NO son repito, NO atajo Es por eso que se recomienda que NO defina estos operadores para sus propias clases.
Martin York
Redefiní estos operadores hace un tiempo, cuando creé una clase que haría algunas operaciones básicas de álgebra booleana. Probablemente debería pegar un comentario de advertencia "¡esto destruye el cortocircuito y la evaluación de izquierda a derecha!" en caso de que me olvide de esto. También sobrecargó * / + y los convirtió en sus sinónimos :-)
Joe Pineda
Tener llamadas de función en un bloque if no es una buena práctica de programación. Siempre tenga una variable declarada que contenga el valor de retorno del método y úsela en el bloque if.
SR Chaitanya
66
@SRChaitanya Eso no es correcto. Lo que describe arbitrariamente como mala práctica se hace todo el tiempo, especialmente con funciones que devuelven booleanos, como aquí.
Marqués de Lorne

Respuestas:

154

Sí, se requiere orden de cortocircuito y evaluación para los operadores ||y &&en los estándares C y C ++.

El estándar C ++ dice (debe haber una cláusula equivalente en el estándar C):

1.9.18

En la evaluación de las siguientes expresiones

a && b
a || b
a ? b : c
a , b

usando el significado incorporado de los operadores en estas expresiones, hay un punto de secuencia después de la evaluación de la primera expresión (12).

En C ++ hay una trampa adicional: el cortocircuito NO se aplica a los tipos que sobrecargan operadores ||y &&.

Nota 12: Los operadores indicados en este párrafo son los operadores integrados, como se describe en la cláusula 5. Cuando uno de estos operadores está sobrecargado (cláusula 13) en un contexto válido, designando así una función de operador definida por el usuario, la expresión designa una invocación de función, y los operandos forman una lista de argumentos, sin un punto de secuencia implícito entre ellos.

Por lo general, no se recomienda sobrecargar estos operadores en C ++ a menos que tenga un requisito muy específico. Puede hacerlo, pero puede romper el comportamiento esperado en el código de otras personas, especialmente si estos operadores se usan indirectamente a través de plantillas de instanciación con el tipo que sobrecarga a estos operadores.

Alex B
fuente
3
No sabía que el cortocircuito no se aplicaría a operaciones lógicas sobrecargadas, eso es muy interesante. ¿Pueden agregar una referencia al estándar o una fuente? No estoy desconfiando de ti, solo quiero aprender más sobre esto.
Joe Pineda
44
Sí, eso es lógico. está actuando como argumentos para el operador && (a, b). Es su implementación lo que dice lo que sucede.
Johannes Schaub - litb
10
litb: Simplemente no es posible pasar b al operador && (a, b) sin evaluarlo. Y no hay forma de deshacer la evaluación de b porque el compilador no puede garantizar que no haya efectos secundarios.
jmucchiello
2
Esto me parece triste. Pensé que si redefiniera los operadores && y || y todavía son completamente deterministas , el compilador lo detectaría y mantendría su evaluación en cortocircuito: después de todo, el orden es irrelevante, ¡y no garantizan efectos secundarios!
Joe Pineda
2
@ Joe: pero el valor de retorno y los argumentos del operador pueden cambiar de booleano a otra cosa. Solía ​​implementar lógica "especial" con TRES valores ("verdadero", "falso" y "desconocido"). El valor de retorno es determinista, pero el comportamiento de cortocircuito no es apropiado.
Alex B
70

La evaluación de cortocircuito y el orden de evaluación es un estándar semántico obligatorio tanto en C como en C ++.

Si no fuera así, un código como este no sería un idioma común

   char* pChar = 0;
   // some actions which may or may not set pChar to something
   if ((pChar != 0) && (*pChar != '\0')) {
      // do something useful

   }

La sección 6.5.13 Operador lógico Y de la especificación C99 (enlace PDF) dice

(4) A diferencia del operador binario bit a bit, el operador && garantiza la evaluación de izquierda a derecha; Hay un punto de secuencia después de la evaluación del primer operando. Si el primer operando se compara igual a 0, el segundo operando no se evalúa.

Del mismo modo, la sección 6.5.14 El operador lógico OR dice

(4) A diferencia del bit a bit | operador, el || el operador garantiza la evaluación de izquierda a derecha; Hay un punto de secuencia después de la evaluación del primer operando. Si el primer operando se compara desigual a 0, el segundo operando no se evalúa.

Se puede encontrar una redacción similar en los estándares de C ++, consulte la sección 5.14 en este borrador de copia . Como los correctores observan en otra respuesta, si anula && o ||, ambos operandos deben evaluarse a medida que se convierte en una llamada de función regular.

Paul Dixon
fuente
Ah, lo que estaba buscando! ¡Bien, entonces el orden de evaluación y el cortocircuito son obligatorios según ANSI-C 99! Realmente me encantaría ver la referencia equivalente para ANSI-C ++, aunque estoy casi al 99%, debe ser lo mismo.
Joe Pineda
Difícil de encontrar un buen enlace gratuito para los estándares C ++, me he vinculado a un borrador que encontré con algunas búsquedas en Google.
Paul Dixon
Verdadero para los tipos de POD. Pero si sobrecarga al operador && o al operador || Estos no son atajos.
Martin York
1
Sí, es interesante notar que para bool, siempre tendrá un orden de evaluación garantizado y un comportamiento de corto circuito porque no puede sobrecargar el operador && para dos tipos integrados. necesita al menos un tipo definido por el usuario en los operandos para que se comporte de manera diferente.
Johannes Schaub - litb
Desearía poder aceptar ambos Checkers y esta respuesta. Como estoy interesado principalmente en C ++, acepto el otro, ¡aunque tengo que admitir que también es excelente! ¡Muchas gracias!
Joe Pineda
19

Sí, lo exige (tanto el orden de evaluación como el cortocircuito). En su ejemplo, si todas las funciones devuelven verdadero, el orden de las llamadas es estrictamente de la función A, luego la función B y luego la función C. Utilizado para esto como

if(ptr && ptr->value) { 
    ...
}

Lo mismo para el operador de coma:

// calls a, then b and evaluates to the value returned by b
// which is used to initialize c
int c = (a(), b()); 

Se dice entre la izquierda y operando de la derecha de &&, ||, ,y entre la primera y la segunda / tercera operando de ?:(operador condicional) es un "punto de secuencia". Cualquier efecto secundario se evalúa completamente antes de ese punto. Entonces, esto es seguro:

int a = 0;
int b = (a++, a); // b initialized with 1, and a is 1

Tenga en cuenta que el operador de coma no debe confundirse con la coma sintáctica utilizada para separar las cosas:

// order of calls to a and b is unspecified!
function(a(), b());

El estándar C ++ dice en 5.14/1:

El operador && se agrupa de izquierda a derecha. Los operandos se convierten implícitamente a tipo bool (cláusula 4). El resultado es verdadero si ambos operandos son verdaderos y falsos de lo contrario. A diferencia de &, && garantiza la evaluación de izquierda a derecha: el segundo operando no se evalúa si el primer operando es falso.

Y en 5.15/1:

El || grupos de operadores de izquierda a derecha. Los operandos se convierten implícitamente a bool (cláusula 4). Devuelve verdadero si alguno de sus operandos es verdadero, y falso de lo contrario. A diferencia de |, || garantiza la evaluación de izquierda a derecha; además, el segundo operando no se evalúa si el primer operando se evalúa como verdadero.

Dice para ambos al lado de aquellos:

El resultado es un bool. Todos los efectos secundarios de la primera expresión, excepto la destrucción de los temporales (12.2) ocurren antes de evaluar la segunda expresión.

Además de eso, 1.9/18dice

En la evaluación de cada una de las expresiones.

  • a && b
  • a || b
  • a ? b : C
  • a , b

utilizando el significado incorporado de los operadores en estas expresiones (5.14, 5.15, 5.16, 5.18), hay un punto de secuencia después de la evaluación de la primera expresión.

Johannes Schaub - litb
fuente
10

Directamente del viejo K&R:

C garantiza eso &&y ||se evalúa de izquierda a derecha; pronto veremos casos en los que esto es importante.

John T
fuente
3
K&R 2da edición p40. "Las expresiones conectadas por && o || se evalúan de izquierda a derecha, y la evaluación se detiene tan pronto como se sepa la verdad o la falsedad del resultado. La mayoría de los programas de C confían en estas propiedades". No puedo encontrar el texto citado en ninguna parte del libro. ¿Es esto de la primera edición extremadamente obsoleta? Por favor, aclare dónde encontró este texto.
Lundin
1
Ok resulta que estás citando este antiguo tutorial . Es de 1974 y altamente irrelevante.
Lundin
6

Ten mucho, mucho cuidado.

Para los tipos fundamentales, estos son operadores de acceso directo.

Pero si define estos operadores para su propia clase o tipos de enumeración, no son atajos. Debido a esta diferencia semántica en su uso en estas circunstancias diferentes, se recomienda no definir estos operadores.

Para los tipos fundamentales operator &&y operator ||para los, el orden de evaluación es de izquierda a derecha (de lo contrario, el atajo sería difícil :-) Pero para los operadores sobrecargados que defina, estos son básicamente azúcar sintáctico para definir un método y, por lo tanto, el orden de evaluación de los parámetros es indefinido

Martin York
fuente
1
La sobrecarga del operador no tiene nada que ver con que el tipo sea POD o no. Para definir una función de operador, al menos uno de los argumentos debe ser una clase (o estructura o unión) o una enumeración, o una referencia a uno de esos. Ser POD significa que puedes usar memcpy en él.
Derek Ledbetter
Y eso es lo que estaba diciendo. Si sobrecarga && para su clase, entonces es realmente solo una llamada al método. Por lo tanto, no puede confiar en el orden de evaluación de los parámetros. Obviamente no puede sobrecargar && para los tipos de POD.
Martin York
3
Está utilizando el término "tipos de POD" incorrectamente. Puede sobrecargar && para cualquier estructura, clase, unión o enumeración, POD o no. No puede sobrecargar && si ambos lados son tipos numéricos o punteros.
Derek Ledbetter
Estaba usando POD como (char / int / float, etc.) no como un POD agregado (que es de lo que está hablando) y generalmente se refiere a forma separada o más explícita porque no es un tipo incorporado.
Martin York
2
¿Entonces quiso decir "tipos fundamentales" pero escribió "tipos POD"?
Öö Tiib
0

Su pregunta se reduce a la precedencia del operador C ++ y la asociatividad. Básicamente, en expresiones con múltiples operadores y sin paréntesis, el compilador construye el árbol de expresión siguiendo estas reglas.

Por precedencia, cuando tiene algo así A op1 B op2 C, puede agrupar las cosas como (A op1 B) op2 Co A op1 (B op2 C). Si op1tiene mayor prioridad que op2, obtendrá la primera expresión. De lo contrario, obtendrás el segundo.

Para la asociatividad, cuando tienes algo así A op B op C, puedes volver a agrupar los delgados como (A op B) op Co A op (B op C). Si opha dejado asociatividad, terminamos con la primera expresión. Si tiene la asociatividad correcta, terminamos con la segunda. Esto también funciona para operadores en el mismo nivel de precedencia.

En este caso particular, &&tiene mayor prioridad que ||, por lo que la expresión se evaluará como (a != "" && it == seqMap.end()) || isEven.

El orden en sí es "de izquierda a derecha" en la forma del árbol de expresión. Entonces primero evaluaremos a != "" && it == seqMap.end(). Si es cierto, toda la expresión es verdadera, de lo contrario vamos a isEven. El procedimiento se repite recursivamente dentro de la subexpresión izquierda, por supuesto.


Tidbits interesantes, pero el concepto de precedencia tiene sus raíces en la notación matemática. Lo mismo sucede en a*b + c, donde *tiene mayor prioridad que +.

Aún más interesante / oscuro, para una expresión no enfatizada A1 op1 A2 op2 ... opn-1 An, donde todos los operadores tienen la misma precedencia, el número de árboles de expresión binaria que podríamos formar viene dado por los llamados números catalanes . Para grandes n, estos crecen extremadamente rápido. re

Horia Coman
fuente
Todo esto es correcto, pero se trata de la precedencia del operador y la asociatividad, no se trata del orden de evaluación y el curcuiting corto. Esas son cosas diferentes.
Thomas Padron-McCarthy
0

Si confías en Wikipedia:

[ &&y ||] son ​​semánticamente distintos de los operadores en cuanto a bits & y | porque nunca evaluarán el operando correcto si el resultado se puede determinar solo desde la izquierda

C (lenguaje de programación)

Sophie Alpert
fuente
11
¿Por qué confiar en wiki cuando tenemos un estándar?
Martin York
1
Si confía en Wikipedia, 'Wikipedia no es un recurso confiable' .
Marqués de Lorne
Esto es cierto hasta donde llega, pero incompleto, ya que los operadores sobrecargados en C ++ no están en cortocircuito.
Thomas Padron-McCarthy