obtuve una respuesta inesperada de la expresión x?y:z

11 minutos de lectura

avatar de usuario
cao lei

Aquí hay un fragmento simple de C++:

int x1 = 10, x2 = 20, y1 = 132, y2 = 12, minx, miny, maxx, maxy;
x1 <= x2 ? minx = x1, maxx = x2 : minx = x2, maxx = x1;
y1 <= y2 ? miny = y1, maxy = y2 : miny = y2, maxy = y1;
cout << "minx=" << minx << "\n";
cout << "maxx=" << maxx << "\n";
cout << "miny=" << miny << "\n";
cout <<" maxy=" << maxy << "\n";

Pensé que el resultado debería ser:

minx=10
maxx=20
miny=12
maxy=132

Pero en realidad el resultado es:

minx=10
maxx=10
miny=12
maxy=132

alguien podria dar una explicacion por que maxx no es 20? Gracias.

  • Los paréntesis resuelven el problema…

    usuario529758

    18 mayo 2013 a las 21:38

  • Otra razón más para “no tratar de ser inteligente” con expresiones condicionales y uso if en cambio. El compilador hará lo mismo de cualquier manera. [assuming you add the relevant parenthesis so that it does what you actually wanted]. Las expresiones ternarias pueden ser útiles a veces, pero este es un buen ejemplo de lo que NO se debe hacer con ellas.

    – Mats Peterson

    18 mayo 2013 a las 21:44

  • (Y de todos modos, por qué no: maxx = x1 > x2 ? x1 : x2?

    usuario529758

    18 mayo 2013 a las 21:44

  • Y por qué no #include<algorithm> y maxi = std::max(x1, x2)?

    –Eric Jablow

    19 de mayo de 2013 a las 0:17

  • Cuando vi por primera vez el título de esta pregunta, pensé que era va a ser sobre PHP.

    – Ray Toal

    19 de mayo de 2013 a las 6:01

avatar de usuario
perreal

Debido a la precedencia del operador, la expresión se analiza de la siguiente manera:

(x1<=x2 ? minx=x1,maxx=x2 : minx=x2), maxx=x1;

puedes resolver esto con:

(x1<=x2) ? (minx=x1,maxx=x2) : (minx=x2, maxx=x1);

Y en realidad no necesitas los primeros dos pares de paréntesis. También revisa esta pregunta.

avatar de usuario
daniel pescador

La precedencia del operador condicional es mayor que la del operador coma, por lo que

x1<=x2 ? minx=x1,maxx=x2 : minx=x2,maxx=x1;

está entre paréntesis como

(x1<=x2 ? minx=x1,maxx=x2 : minx=x2),maxx=x1;

Por lo tanto, la última asignación se realiza independientemente de la condición.

Para solucionarlo, puede

  • usar paréntesis:

    x1 <= x2 ? (minx = x1, maxx = x2) : (minx = x2, maxx = x1);
    

    (usted no necesitar los paréntesis en el true rama, pero en mi opinión, es mejor tenerlos también).

  • Usa dos condicionales:

    minx = x1 <= x2 ? x1 : x2;
    maxx = x1 <= x2 ? x2 : x1;
    
  • usar un if:

    if (x1 <= x2) {
        minx = x1;
        maxx = x2;
    } else {
        minx = x2;
        maxx = x1;
    }
    

Compilado con o sin optimizaciones, el if version y el condicional simple entre paréntesis con comas producen el mismo ensamblaje tanto en gcc (4.7.2) como en clang (3.2), es razonable esperar eso también de otros compiladores. La versión con los dos condicionales produce un ensamblaje diferente, pero con optimizaciones, ambos compiladores emiten solo uno cmp instrucción para eso también.

En mi opinión, el if La versión es la más fácil de verificar, por lo que es preferible.

  • +1 para probar la diferencia en el código generado y proporcionar un argumento de legibilidad.

    – esponjoso

    19 de mayo de 2013 a las 6:14

avatar de usuario
esteras petersson

Mientras que otros han explicado cuál es la causa del problema, creo que la solución “mejor” debería ser escribir el condicional con si:

int x1 = 10, x2=20, y1=132, y2=12, minx, miny, maxx, maxy;
if (x1<=x2) 
{ 
   minx=x1;
   maxx=x2;
}
else
{
   minx=x2; 
   maxx=x1;
}
if (y1<=y2)
{
    miny=y1;
    maxy=y2;
} 
else 
{
    miny=y2;
    maxy=y1;
}

Sí, es varias líneas más largo, pero también es más fácil de leer y borrar exactamente lo que sucede (y si necesita recorrerlo paso a paso en el depurador, puede ver fácilmente en qué dirección va).

Cualquier compilador moderno debería poder convertir cualquiera de estos en asignaciones condicionales bastante eficientes que hagan un buen trabajo al evitar las bifurcaciones (y, por lo tanto, la “mala predicción de bifurcaciones”).

Preparé una pequeña prueba, que compilé usando

g++ -O2 -fno-inline -S -Wall ifs.cpp

Aquí está la fuente (tuve que convertirla en parámetros para asegurarme de que el compilador no solo calculara el valor correcto directamente y simplemente hiciera mov $12,%rdxpero en realidad hizo una comparación y decidió con es más grande):

void mine(int x1, int x2, int y1, int y2)
{
    int minx, miny, maxx, maxy;
    if (x1<=x2) 
    { 
    minx=x1;
    maxx=x2;
    }
    else
    {
    minx=x2; 
    maxx=x1;
    }
    if (y1<=y2)
    {
    miny=y1;
    maxy=y2;
    } 
    else 
    {
    miny=y2;
    maxy=y1;
    }

    cout<<"minx="<<minx<<"\n";
    cout<<"maxx="<<maxx<<"\n";
    cout<<"miny="<<miny<<"\n";
    cout<<"maxy="<<maxy<<"\n";
}

void original(int x1, int x2, int y1, int y2)
{
    int minx, miny, maxx, maxy;
    x1<=x2 ? (minx=x1,maxx=x2) : (minx=x2,maxx=x1);
    y1<=y2 ? (miny=y1,maxy=y2) : (miny=y2,maxy=y1);
    cout<<"minx="<<minx<<"\n";
    cout<<"maxx="<<maxx<<"\n";
    cout<<"miny="<<miny<<"\n";
    cout<<"maxy="<<maxy<<"\n";
}

void romano(int x1, int x2, int y1, int y2)
{
    int  minx, miny, maxx, maxy;

    minx = ((x1 <= x2) ? x1 : x2);
    maxx = ((x1 <= x2) ? x2 : x1);
    miny = ((y1 <= y2) ? y1 : y2);
    maxy = ((y1 <= y2) ? y2 : y1);
    cout<<"minx="<<minx<<"\n";
    cout<<"maxx="<<maxx<<"\n";
    cout<<"miny="<<miny<<"\n";
    cout<<"maxy="<<maxy<<"\n";
}

int main()
{
    int x1=10, x2=20, y1=132, y2=12;
    mine(x1, x2, y1, y2);
    original(x1, x2, y1, y2);
    romano(x1, x2, y1, y2);
    return 0;
}

El código generado se ve así:

_Z4mineiiii:
.LFB966:
    .cfi_startproc
    movq    %rbx, -32(%rsp)
    movq    %rbp, -24(%rsp)
    movl    %ecx, %ebx
    movq    %r12, -16(%rsp)
    movq    %r13, -8(%rsp)
    movl    %esi, %r12d
    subq    $40, %rsp
    movl    %edi, %r13d
    cmpl    %esi, %edi
    movl    %edx, %ebp
    cmovg   %edi, %r12d
    cmovg   %esi, %r13d
    movl    $_ZSt4cout, %edi
    cmpl    %ecx, %edx
    movl    $.LC0, %esi
    cmovg   %edx, %ebx
    cmovg   %ecx, %ebp
        .... removed actual printout code that is quite long and unwieldy... 
_Z8originaliiii:
    movq    %rbx, -32(%rsp)
    movq    %rbp, -24(%rsp)
    movl    %ecx, %ebx
    movq    %r12, -16(%rsp)
    movq    %r13, -8(%rsp)
    movl    %esi, %r12d
    subq    $40, %rsp
    movl    %edi, %r13d
    cmpl    %esi, %edi
    movl    %edx, %ebp
    cmovg   %edi, %r12d
    cmovg   %esi, %r13d
movl    $_ZSt4cout, %edi
cmpl    %ecx, %edx
movl    $.LC0, %esi
cmovg   %edx, %ebx
cmovg   %ecx, %ebp
        ... print code goes here ... 
_Z6romanoiiii:
    movq    %rbx, -32(%rsp)
    movq    %rbp, -24(%rsp)
    movl    %edx, %ebx
    movq    %r12, -16(%rsp)
    movq    %r13, -8(%rsp)
    movl    %edi, %r12d
    subq    $40, %rsp
    movl    %esi, %r13d
    cmpl    %esi, %edi
    movl    %ecx, %ebp
    cmovle  %edi, %r13d
    cmovle  %esi, %r12d
movl    $_ZSt4cout, %edi
cmpl    %ecx, %edx
movl    $.LC0, %esi
cmovle  %edx, %ebp
cmovle  %ecx, %ebx
        ... printout code here.... 

Como puedes ver, mine y original es idéntico, y romano utiliza registros ligeramente diferentes y una forma diferente de cmovpero por lo demás hacen lo mismo en el mismo número de instrucciones.

  • +1. El código publicado en la pregunta es exactamente el tipo de código que hace que muchas organizaciones simplemente prohíban el operador ternario por completo. El operador ternario tiene su lugar, pero el uso publicado no es ese lugar.

    – David Hamman

    18 mayo 2013 a las 22:01


  • Re Sí, son varias líneas más largas. — No sería tan malo si hubiera usado 1TBS. 🙂

    – David Hamman

    18 mayo 2013 a las 22:04

  • @DavidHammen Sí, puedes diseñarlo de otra manera [this is just how I usually write my own code], pero probablemente no (sin hacerlo bastante complicado) reducirlo a 2 líneas … Incluso cuatro líneas razonablemente legibles lo están empujando. Entonces, la declaración sigue siendo “varias líneas más”. Y mi punto era hacerlo más legible, no algo adecuado para una entrada de IOCCC.

    – Mats Peterson

    18 mayo 2013 a las 22:11

  • +1. El operador condicional no se veía bien aquí. Publiqué otra variante para esto con operadores condicionales ‘cortos’ que mantienen el código tan legible/compacto como podría ser. Lo interesante aquí es cómo se generará el código de máquina.

    – Roman Nikitchenko

    18 mayo 2013 a las 22:12

  • Una ventaja del operador condicional es que hace mucho más probable que el compilador utilice el cmov operaciones que eliminan posibles burbujas de canalización del código.

    – Zan Lince

    18 mayo 2013 a las 22:23

avatar de usuario
Roman Nikitchenko

Pregunta interesante tanto sobre la precedencia de operaciones como sobre la generación de código.

OK, , operación tiene MUY baja prioridad (la más baja, consulte tabla de referencia). Debido a este hecho, su código es el mismo que el de las siguientes líneas:

((x1<=x2) ? minx=x1,maxx=x2 : minx=x2),maxx=x1;
((y1<=y2) ? miny=y1,maxy=y2 : miny=y2),maxy=y1;

En realidad, solo la gramática C/C++ previene primero , del mismo comportamiento.

Otro lugar realmente peligroso en la precedencia de las operaciones C/C++ son las operaciones bit a bit y la comparación. Considere el siguiente fragmento:

int a = 2;
int b = (a == 2|1); // Looks like check for expression result? Nope, results 1!

De cara al futuro, recomendaría volver a escribir su fragmento de esta manera manteniendo el equilibrio entre la eficiencia y la legibilidad:

minx = ((x1 <= x2) ? x1 : x2);
maxx = ((x1 <= x2) ? x2 : x1);
miny = ((y1 <= y2) ? y1 : y2);
maxy = ((y1 <= y2) ? y2 : y1);

El hecho más interesante sobre este fragmento de código es que dicho estilo podría resultar en el código más efectivo para algunas arquitecturas como ARM debido a los indicadores de bits de condición en el conjunto de instrucciones de la CPU (la duplicación de condición no cuesta nada y más, apunta al compilador a los fragmentos de código correctos).

Debido a la precedencia del operador:

(x1<=x2 ? minx=x1,maxx=x2 : minx=x2),maxx=x1

Puedes arreglarlo con:

int x1=10, x2=20, y1=132, y2=12, minx, miny, maxx, maxy;
x1<=x2 ? (minx=x1,maxx=x2) : (minx=x2,maxx=x1);
y1<=y2 ? (miny=y1,maxy=y2) : (miny=y2,maxy=y1);
cout<<"minx="<<minx<<"\n";
cout<<"maxx="<<maxx<<"\n";
cout<<"miny="<<miny<<"\n";
cout<<"maxy="<<maxy<<"\n";

En C++ 11 puedes usar std::tie y std::make_pair para hacer esto obviamente correcto de un vistazo (TM)

#include <tuple>
#include <utility>
#include <iostream>

using namespace std;

int main()
{
    int x1 = 10, x2=20, y1=132, y2=12, minx, miny, maxx, maxy;

    tie(minx, maxx) = (x1 <= x2)? make_pair(x1, x2) : make_pair(x2, x1);
    tie(miny, maxy) = (y1 <= y2)? make_pair(y1, y2) : make_pair(y2, y1);

    cout<<"minx="<<minx<<"\n";
    cout<<"maxx="<<maxx<<"\n";
    cout<<"miny="<<miny<<"\n";
    cout<<"maxy="<<maxy<<"\n";
}

En línea producción.

Esto es semánticamente equivalente a todas las demás soluciones publicadas, y con cualquier compilador de optimización decente, no tiene ningún tipo de sobrecarga. Es sintácticamente mucho mejor porque tiene

  • un mínimo de repetición de código,
  • las 4 variables asignadas a están todas en el lado izquierdo de la asignación, y
  • las 4 variables de origen asignadas están todas a la derecha.

Como una ligera variación que se generaliza para encontrar punteros al elemento mínimo y máximo de las secuencias, podría usar std::minmax_element y el hecho de que las matrices sin procesar tienen funciones que no son miembros begin() y end() (ambas funciones de C++ 11)

#include <algorithm>
#include <tuple>
#include <iostream>

using namespace std;

int main()
{
    int x[] = { 10, 20 }, y[] = { 132, 12 }, *minx, *miny, *maxx, *maxy;

    tie(minx, maxx) = minmax_element(begin(x), end(x));
    tie(miny, maxy) = minmax_element(begin(y), end(y));

    cout<<"minx="<<*minx<<"\n";
    cout<<"maxx="<<*maxx<<"\n";
    cout<<"miny="<<*miny<<"\n";
    cout<<"maxy="<<*maxy<<"\n";
}

En línea producción.

¿Ha sido útil esta solución?