¿Por qué es a = (a+b) - (b=a) una mala opción para intercambiar dos enteros?


Me topé con este código para intercambiar dos enteros sin usar una variable temporal o el uso de operadores bitwise.

int main(){

    int a=2,b=3;
    printf("a=%d,b=%d",a,b);
    a=(a+b)-(b=a);
    printf("\na=%d,b=%d",a,b);
    return 0;
}

Pero creo que este código tiene un comportamiento indefinido en la instrucción swap a = (a+b) - (b=a); ya que no contiene ningún puntos de secuencia para determinar el orden de evaluación.

Mi pregunta es: ¿Es esta una solución aceptable para intercambiar dos enteros?

Author: ashfaque, 2013-12-27

10 answers

No. Esto no es aceptable. Este código invoca Comportamiento indefinido. Esto se debe a que la operación en b no está definida. En la expresión

a=(a+b)-(b=a);  

No es seguro si b se modifica primero o si su valor se usa en la expresión (a+b) debido a la falta del punto de secuencia .
Ver qué syas estándar:

C11: 6.5 Expresiones:

Si un efecto secundario en un objeto escalar no se ya sea un efecto secundario diferente en el mismo objeto escalar o un cálculo de valor utilizando el valor del mismo escalar objeto, el comportamiento es indefinido. Si hay múltiples órdenes permitidas de la subexpresiones de una expresión, el comportamiento es indefinido si un lado no secuenciado el efecto se produce en cualquiera de los pedidos.84)1.

Lea C-faq-3.8 y esta respuesta para una explicación más detallada del punto de secuencia y comportamiento indefinido.


1. El énfasis es mío.

 83
Author: haccks,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:53:17

Mi pregunta es - ¿Es esta una solución aceptable para intercambiar dos enteros?

Aceptable para quién? Si estás preguntando si es aceptable para mí, eso no pasaría de ninguna revisión de código en la que estuve, créeme.

¿Por qué es a=(a+b)-(b=a) una mala opción para intercambiar dos enteros?

Por las siguientes razones:

1) Como usted nota, no hay garantía en C que realmente hace eso. Podría hacer cualquier cosa.

2) Supongamos por el bien de argumento de que realmente intercambia dos enteros, como lo hace en C#. (C# garantiza que los efectos secundarios ocurren de izquierda a derecha.) El código todavía sería inaceptable porque no es completamente obvio cuál es su significado! El código no debería ser un montón de trucos inteligentes. Escriba el código para la persona que viene después de usted que tiene que leerlo y entenderlo.

3) De nuevo, supongamos que funciona. El código sigue siendo inaceptable porque esto es simplemente falso:

Me tropecé con este código para intercambiar dos enteros sin usar una variable temporal o el uso de operadores de bits.

Eso es simplemente falso. Este truco utiliza una variable temporal para almacenar el cálculo de a+b. La variable es generada por el compilador en su nombre y no se le da un nombre, pero está ahí. Si el objetivo es eliminar temporales, esto lo hace peor, no mejor! ¿Y por qué querrías eliminar los temporales en primer lugar? Son baratos!

4) Esto solo funciona para enteros. Muchas cosas necesitan ser intercambiadas aparte de los enteros.

En resumen, dedique su tiempo a concentrarse en escribir código que es obviamente correcto, en lugar de tratar de idear trucos inteligentes que realmente empeoren las cosas.

 48
Author: Eric Lippert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-12-27 18:30:15

Hay al menos dos problemas con a=(a+b)-(b=a).

Uno que usted mismo menciona: la falta de puntos de secuencia significa que el comportamiento es indefinido. Como tal, cualquier cosa podría suceder. Por ejemplo, no hay garantía de que se evalúa primero: a+b o b=a. El compilador puede elegir generar código para la tarea primero, o hacer algo completamente diferente.

Otro problema es el hecho de que el desbordamiento de la aritmética con signos es un comportamiento indefinido. Si a+b se desborda no hay garantía de los resultados; incluso se podría lanzar una excepción.

 32
Author: Joni,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-12-27 18:14:29

Aparte de las otras respuestas, sobre el comportamiento y el estilo indefinidos, si escribe código simple que solo usa una variable temporal, el compilador probablemente puede rastrear los valores y no intercambiarlos en el código generado, y simplemente usar los valores intercambiados más adelante en algunos casos. No puede hacer eso con tu código. El compilador es generalmente mejor que usted en micro optimizaciones.

Así que es probable que su código sea más lento, más difícil de entender y probablemente un comportamiento indefinido poco fiable demasiado.

 29
Author: jcoder,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-12-27 13:03:50

Si usas gcc y -Wall el compilador ya te avisa

A. c:3: 26: advertencia: la operación en 'b' puede ser indefinida [-Wsequence-point]

Si usar tal construcción es discutible desde un punto de rendimiento también. Cuando nos fijamos en

void swap1(int *a, int *b)
{
    *a = (*a + *b) - (*b = *a);
}

void swap2(int *a, int *b)
{
    int t = *a;
    *a = *b;
    *b = t;
}

Y examinar el código de ensamblaje

swap1:
.LFB0:
    .cfi_startproc
    movl    (%rdi), %edx
    movl    (%rsi), %eax
    movl    %edx, (%rsi)
    movl    %eax, (%rdi)
    ret
    .cfi_endproc

swap2:
.LFB1:
    .cfi_startproc
    movl    (%rdi), %eax
    movl    (%rsi), %edx
    movl    %edx, (%rdi)
    movl    %eax, (%rsi)
    ret
    .cfi_endproc

No se puede ver ningún beneficio para ofuscar el código.


Mirando el código C++ (g++), que hace básicamente lo mismo, pero toma move en cuenta

#include <algorithm>

void swap3(int *a, int *b)
{
    std::swap(*a, *b);
}

Da una salida de ensamblaje idéntica

_Z5swap3PiS_:
.LFB417:
    .cfi_startproc
    movl    (%rdi), %eax
    movl    (%rsi), %edx
    movl    %edx, (%rdi)
    movl    %eax, (%rsi)
    ret
    .cfi_endproc

Teniendo en cuenta la advertencia del CCG y no viendo ninguna ganancia técnica, yo diría que se adhieren a las técnicas estándar. Si esto alguna vez se convierte en un cuello de botella, todavía se puede investigar, cómo mejorar o evitar esta pequeña pieza de código.

 28
Author: Olaf Dietsche,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-04-18 10:09:43

La declaración:

a=(a+b)-(b=a);

Invoca un comportamiento indefinido. El segundo deberá en el párrafo citado se viola:

(C99, 6.5p2) "Entre el punto de secuencia anterior y el siguiente un objeto tendrá su valor almacenado modificado a lo sumo una vez por la evaluación de una expresión. Además, el valor anterior se leerá únicamente para determinar el valor que se almacenará."

 8
Author: ouah,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-12-27 12:33:45

Una pregunta fue publicada de nuevo en 2010 con el mismo ejemplo.

a = (a+b) - (b=a);

Steve Jessop advierte contra ello:

El comportamiento de ese código es indefinido, por cierto. Tanto a como b se leen y escrito sin un punto de secuencia intermedio. Para empezar, el compilador estaría bien dentro de sus derechos para evaluar b = a antes evaluación de a + b.

Aquí hay una explicación de una pregunta publicada en 2012. Tenga en cuenta que la muestra no es exactamente lo mismo debido a la falta de paréntesis, pero la respuesta sigue siendo relevante.

En C++, las subexpresiones en expresiones aritméticas no tienen temporal ordenar.

a = x + y;

¿x se evalúa primero, o y? El compilador puede elegir cualquiera, o puede elige algo completamente diferente. El orden de evaluación no es lo mismo que la precedencia del operador: la precedencia del operador es estrictamente definido, y el orden de evaluación solo se define según la granularidad que su programa tiene puntos de secuencia.

De hecho, en algunas arquitecturas es posible emitir código que evalúa x e y al mismo tiempo -- por ejemplo, VLIW arquitectura.

Ahora para C11 cotizaciones estándar de N1570:

Anexo J. 1 / 1

Es un comportamiento no especificado cuando:

- La orden en qué subexpresiones se evalúan y el orden en qué lado efectos tienen lugar, excepto como se especifica para la función-llamada (), &&, ||, ? :, y operadores de coma (6.5).

- El orden en el que se evalúan los operandos de un operador de asignación (6.5.16).

Anexo J. 2 / 1

Es un comportamiento indefinido cuando:

- Un efecto secundario en un objeto escalar no tiene efecto secundario diferente en el mismo objeto escalar o un cálculo de valor usando el valor del mismo escalar objeto (6.5).

6.5/1

Una expresión es una secuencia de operadores y operandos que especifica cálculo de un valor, o que designa un objeto o una función, o que genera efectos secundarios, o que realiza una combinación de los mismos. Los cálculos de valor de los operandos de un operador se secuencian antes del cálculo del valor del resultado del operador.

6.5/2

Si un efecto secundario en un objeto escalar es no secuenciado en relación con cualquiera un efecto secundario diferente en el mismo objeto escalar o un valor cálculo utilizando el valor del mismo objeto escalar, el comportamiento es indefinido. Si hay múltiples orderings permitidos de la subexpresiones de una expresión, el comportamiento es indefinido si tal unsequenced efecto secundario ocurre en cualquiera de los orderings.84)

6.5/3

La agrupación de operadores y operandos se indica mediante la sintaxis.85) Excepto como especificado más adelante, los efectos secundarios y los cálculos de valor de las subexpresiones no tienen secuencia.86)

No debes confiar en un comportamiento indefinido.

Algunas alternativas: En C++ puedes usar

  std::swap(a, b);

XOR-swap:

  a = a^b;
  b = a^b;
  a = a^b;
 8
Author: Community,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:17:05

El problema es que de acuerdo con el estándar de C++

Excepto donde se indique, evaluaciones de operandos de operadores individuales y de las subexpresiones de expresiones individuales no están secuenciadas.

Así que esta expresión

a=(a+b)-(b=a);

Tiene un comportamiento indefinido.

 5
Author: Vlad from Moscow,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-12-27 12:31:44

Puede usar el algoritmo de intercambio XOR para evitar cualquier problema de desbordamiento y seguir teniendo una línea única.

Pero, ya que tienes una etiqueta c++, preferiría solo una simple std::swap(a, b) para que sea más fácil de leer.

 5
Author: dreamzor,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-12-29 07:54:34

Además de los problemas invocados por los otros usuarios, este tipo de swap tiene otro problema: el dominio.

¿Qué pasa si a+b va más allá de su límite? Digamos que trabajamos con el número de 0 a 100. 80+ 60 estaría fuera de rango.

 4
Author: catalinux,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-12-28 13:01:30