¿Es seguro poner operadores de incremento/decremento dentro de operadores ternarios/condicionales?

6 minutos de lectura

aquí hay un ejemplo

#include <iostream>
using namespace std;
int main()
{   
    int x = 0;
    cout << (x == 0 ? x++ : x) << endl; //operator in branch
    cout << "x=" << x << endl;
    cout << (x == 1 || --x == 0 ? 1 : 2) << endl; //operator in condition
    cout << "x=" << x << endl;
    return 0;
}

producción:

0
x=1
1
x=1

Entiendo la salida, pero ¿Es este comportamiento indefinido o no? ¿Está garantizado el orden de evaluación en cualquiera de los dos casos?

Incluso si está garantizado, soy muy consciente de que el uso de incrementos/decrementos puede convertirse rápidamente en un problema de legibilidad. Solo pregunto porque vi un código similar y no estaba seguro de inmediato, dado que hay muchos ejemplos de uso ambiguo/indefinido de operadores de incremento/decremento, como…

  • C++ no define el orden en que se evalúan los parámetros de la función.

    int nValue = Add(x, ++x);
    
  • El lenguaje C++ dice que no puede modificar una variable más de una vez entre puntos de secuencia.

     x = ++y + y++
    
  • Dado que los operadores de incremento y decremento tienen efectos secundarios, el uso de expresiones con operadores de incremento o decremento en una macro de preprocesador puede tener resultados no deseados.

     #define max(a,b) ((a)<(b))?(b):(a)
     k = max( ++i, j );
    

  • Relacionado (no duplicado): stackoverflow.com/questions/10995445/… – Describe un caso especial de asignación de nuevo en la variable incrementada.

    – Suma

    08/09/2014 a las 13:59

  • Nota: Bien definida es solo una pregunta. Mantenible es otro. Si tiene que preguntarnos, ¿cómo se asegurará la próxima persona que lea ese código de que es seguro? “Los verdaderos escritores reescriben para evitar el problema”.

    – keshlam

    9 de septiembre de 2014 a las 3:12

  • El operador de decremento en la línea 4 de main() es irrelevante en este ejemplo porque el comportamiento de cortocircuito de || hará que el --x para ser salteado por completo.

    – JLRishe

    9 de septiembre de 2014 a las 7:43

  • @JLRishe, en realidad, la pregunta gira en torno a eso: ¿el cortocircuito está garantizado de tal manera que --x nunca se evalúa? (Respondido abajo)

    – jozxyqk

    9 de septiembre de 2014 a las 7:53

  • @jozxyqk Ok, pero en ese caso, no tiene nada que ver con operadores ternarios/condicionales y todo que ver con el || operador. x == 1 || --x == 0 se evalúa completamente antes de que el operador condicional tenga alguna participación y en ese momento, el --x ya se habrá saltado. En otras palabras, la línea 4 no nos dice nada que no sea trivial acerca de los operadores condicionales.

    – JLRishe

    9 de septiembre de 2014 a las 7:55


avatar de usuario
CT

Para el operador condicional (§5.16 [expr.cond]/p1):

Cada cálculo de valor y efecto secundario asociado con la primera expresión se secuencia antes de cada cálculo de valor y efecto secundario asociado con la segunda o tercera expresión.

Para el operador lógico OR (§5.15 [expr.log.or]/p1-2):

el segundo operando no se evalúa si el primer operando se evalúa como true.
[…] Si se evalúa la segunda expresión, cada cálculo de valor y efecto secundario asociado con la primera expresión se secuencia antes de cada cálculo de valor y efecto secundario asociado con la segunda expresión.

El comportamiento de su código está bien definido.

avatar de usuario
Vaca Efectiva

Existe un orden de ejecución garantizado en operadores ternarios y booleanos && y || operaciones, por lo que no hay conflicto en los puntos de secuencia de evaluación.

Uno a la vez

 cout << (x == 0 ? x++ : x) << endl; //operator in branch

Siempre saldrá x pero lo incrementará solo si fuera 0.

 cout << (x == 1 || --x == 0 ? 1 : 2) << endl; //operator in condition

Esto también está bien definido, si x fuera 1 no evaluará el RHS, si no lo fuera lo decrementará pero --x nunca será 0, por lo que será verdadero iff x==1, en cuyo caso x ahora también será 0.

En el último caso si x es INT_MIN no es un comportamiento bien definido para disminuirlo (y se ejecutaría).

Eso no puede suceder en el primer caso donde x no será 0 si es INT_MAX para que estés a salvo.

  • Edge case, pero por supuesto el segundo fragmento de código tendría UB si x es INT_MIN cuando se ejecuta. El problema equivalente con INT_MAX no puede afectar el primer fragmento.

    –Steve Jessop

    8 sep 2014 a las 14:06


avatar de usuario
trucos

Entiendo el resultado, pero ¿es este comportamiento indefinido o no?

El código está perfectamente definido. El estándar C11 dice:

6.5.15 Operador condicional

Se evalúa el primer operando; hay un punto de secuencia entre su evaluación y la evaluación del segundo o tercer operando (lo que se evalúe). El segundo operando se evalúa solo si el primero se compara diferente a 0; el tercer operando se evalúa solo si el primero se compara igual a 0; el resultado es el valor del segundo o tercer operando (el que se evalúe), convertido al tipo descrito a continuación.110)

6.5.14 Operador lógico OR

A diferencia de bit a bit | operador, la || el operador garantiza la evaluación de izquierda a derecha; si se evalúa el segundo operando, hay un punto de secuencia entre las evaluaciones del primer y segundo operandos. Si el primer operando se compara desigual a 0el segundo operando no se evalúa.

Más lejos wiki lo explica con un ejemplo:

  • Entre la evaluación de los operandos izquierdo y derecho del && (Y lógico), || (OR lógico) (como parte de la evaluación de cortocircuito), y comma operators. Por ejemplo, en la expresión *p++ != 0 && *q++ != 0todos los efectos secundarios de la subexpresión *p++ != 0 se completan antes de cualquier intento de acceder q.

  • Entre la evaluación del primer operando del operador ternario “signo de interrogación” y el segundo o tercer operando. Por ejemplo, en la expresión a = (*p++) ? (*p++) : 0 hay un punto de secuencia después del primero *p++lo que significa que ya se ha incrementado en el momento en que se ejecuta la segunda instancia.

La regla para || y ?: es lo mismo para C++ (sección 5.15 y 5.16) que en C.


¿Está garantizado el orden de evaluación en cualquiera de los dos casos?

Sí. El orden de evaluación de los operandos de los operadores. ||, &&, , y ?: se garantiza que es de izquierda a derecha.

  • @Slava; ¡Vaya! ¿Cómo es engañoso? Podrías explicar ?

    – trucos

    08/09/2014 a las 14:00


  • Hay 2 ejemplos en el código, el segundo tiene UB imo. Podría estar equivocado, pero la respuesta no responde de todos modos, ya que se basa en si el operador booleano tiene un punto de secuencia.

    – Eslava

    08/09/2014 a las 14:00


  • @Slava; ¿Cuál tiene UB?

    – trucos

    8 de septiembre de 2014 a las 14:04

  • @haccks no hay UB Me equivoqué. Pero su respuesta no menciona por qué el segundo ejemplo no tiene UB, es decir, hay un punto de secuencia en el operador booleano ||

    – Eslava

    08/09/2014 a las 14:09

avatar de usuario
ani627

En C, el valor almacenado de un objeto se puede modificar solo una vez entre dos puntos de secuencia.

Se produce un punto de secuencia:

  1. Al final de la expresión completa.
  2. En el &&, || y ?: operadores
  3. En una llamada de función.

Entonces, por ejemplo, esta expresión x = i++ * i++ es indefinidomientras x = i++ && i++ es perfectamente legal.

Tu código muestra comportamiento definido.

intx=0;

cout

En la expresión anterior x es 0asi que x++ se ejecutará, aquí x ++ es un incremento posterior, por lo que generará 0.

cout

En la expresión anterior como x ahora tiene valor 1 por lo que la salida será 1.

cout

Aquí x es 1 por lo que la siguiente condición no se evalúa (--x == 0) y la salida será 1.

cout

como la expresión --x == 0 no se evalúa, la salida volverá a ser 1.

avatar de usuario
Nicolás Miller

Sí, es seguro usar los operadores de incremento/decremento como los tiene. Esto es lo que está sucediendo en su código:

Fragmento #1

cout << (x == 0 ? x++ : x) << endl; //operator in branch

En este fragmento, está probando si x == 0cual es true. Ya que es truesu expresión ternaria evalúa el x++. Dado que está utilizando un incremento posterior aquí, el valor original para x se imprime en el flujo de salida estándar, y entonces x se incrementa.

Fragmento #2

cout << (x == 1 || --x == 0 ? 1 : 2) << endl; //operator in condition

Este fragmento es un poco más confuso, pero aun así arroja un resultado predecible. En este punto, x = 1 del primer fragmento. En la expresión ternaria, la parte condicional se evalúa primero; sin embargo, debido a Cortocircuito, la segunda condición, --x == 0nunca se evalúa.

Para C++ los operadores || y && son los operadores booleanos de cortocircuito para O lógico y Y lógico respectivamente. Cuando utiliza estos operadores, sus condiciones se verifican (de izquierda a derecha) hasta que se pueda determinar el resultado final. Una vez que se determina el resultado, no se verifican más condiciones.

Mirando el fragmento #2, su primera condición verifica si x == 1. Dado que su primera condición se evalúa como true y está usando el OR lógico, no hay necesidad de seguir evaluando otras condiciones. Eso significa que --x == 0 es nunca ejecutado.


Una nota al margen rápida sobre el cortocircuito:

El cortocircuito es útil para aumentar el rendimiento de su programa. Suponga que tiene una condición como esta que llama a varias funciones que consumen mucho tiempo:

if (task1() && task2())
{ 
    //...Do something...
}

En este ejemplo, task2 Nunca debe ser llamado a menos que task1 se completa con éxito (task2 depende de algunos datos que son alterados por task1).

porque estamos usando un Operador AND en cortocircuitosi task1 falla al regresar false, entonces la declaración if tiene la información suficiente para salir antes y dejar de verificar otras condiciones. Esto significa que task2 nunca se llama.

¿Ha sido útil esta solución?