Operadores incremento/decremento en prefijo y postfijo.

Iniciado por K-YreX, 2 Octubre 2019, 14:46 PM

0 Miembros y 4 Visitantes están viendo este tema.

K-YreX

Ya sé que este suele ser un tema básico pero me he encontrado con un resultado inesperado en el funcionamiento de estos operadores.
Mi problema es que estaba implementando un algoritmo de ordenamiento por inserción tanto en C++ como en Java; sin embargo, el resultado obtenido en C++ no era el que esperaba por eso pongo la duda en este foro.
Aquí el código en Java:
Código (java) [Seleccionar]

public static void insertionSort(int[] numbers){
   for(int i = 1; i < numbers.length; ++i){
       int currentValue = numbers[i];
       int previousIndex = i-1;
       while(previousIndex >= 0 && currentValue < numbers[previousIndex])
           numbers[previousIndex+1] = numbers[previousIndex--];
       numbers[previousIndex+1] = currentValue;
       showArray(numbers);
   }
}


Y aquí el código en C++:
Código (cpp) [Seleccionar]

void insertionSort(int *numbers, const int size){
   for(size_t i = 1; i < size; ++i){
       int currentValue = numbers[i];
       int previousIndex = i-1;
       while(previousIndex >= 0 && currentValue < numbers[previousIndex])
           numbers[previousIndex+1] = numbers[previousIndex--];
       numbers[previousIndex+1] = currentValue;
       showArray(numbers, size);
   }
}

Como se puede ver, códigos idénticos. El problema está en la línea 6 del código en C++. He visto que había valores que se repetían y después de un rato investigando he visto que si en C++ se cambia la línea 6 por:
Código (cpp) [Seleccionar]

numbers[previousIndex+2] = numbers[previousIndex--];

Entonces sí funciona. Y si se cambia el bucle <while> por:
Código (cpp) [Seleccionar]

while(previousIndex >= 0 && currentValue < numbers[previousIndex]){
   numbers[previousIndex+1] = numbers[previousIndex];
   --previousIndex;
}

También funciona correctamente. Entonces el problema sé que está en el orden en que se realiza el decremento. Me gustaría sabes cómo funciona eso en profundidad ya que veo que no funciona igual en Java que en C++.

PD: Agradecería también una explicación del funcionamiento en Java para ver cuáles son las diferencias exactamente. O si es mejor que abra otro tema en el foro de Java para esta parte me lo podéis decir también. :-X :-X
Código (cpp) [Seleccionar]

cout << "Todos tenemos un defecto, un error en nuestro código" << endl;

engel lex

aqui hay un tema que se explicó con un poco de detalle el asunto relacionado a los incrementos/decrementos post/pre, esto es un problema comun a descubrir cuando se está aprendiendo

https://foro.elhacker.net/buscador-t483138.0.html

no es un asunto de java vs c++ es un asunto de nivel mas bajo...
El problema con la sociedad actualmente radica en que todos creen que tienen el derecho de tener una opinión, y que esa opinión sea validada por todos, cuando lo correcto es que todos tengan derecho a una opinión, siempre y cuando esa opinión pueda ser ignorada, cuestionada, e incluso ser sujeta a burla, particularmente cuando no tiene sentido alguno.

@XSStringManolo

Yo por lo que ten entendido en C++ operadores unarios en preincremento la ejecución es más rápida. No sé nada más.

engel lex

Cita de: string Manolo en  2 Octubre 2019, 17:05 PM
Yo por lo que ten entendido en C++ operadores unarios en preincremento la ejecución es más rápida. No sé nada más.

XD claramente entonces lo que sabes es mito XD

no es mas rápida en ningún sentido
El problema con la sociedad actualmente radica en que todos creen que tienen el derecho de tener una opinión, y que esa opinión sea validada por todos, cuando lo correcto es que todos tengan derecho a una opinión, siempre y cuando esa opinión pueda ser ignorada, cuestionada, e incluso ser sujeta a burla, particularmente cuando no tiene sentido alguno.

@XSStringManolo


MAFUS

Sí, parece que el postincremento genera más instrucciones.

Según https://godbolt.org que trabaja con gcc, si no se dan optimizaciones tenemos que el siguiente código int main() {
    int a = 0;
    int b = 0;
    int m = ++a;
    int n = b++;
}


se traduce a
Código (asm) [Seleccionar]
main:
        ; prepara el stack para la función
        push    rbp
        mov     rbp, rsp
        ; int a = 0;
        mov     DWORD PTR [rbp-4], 0
        ; int b = 0;
        mov     DWORD PTR [rbp-8], 0
        ; int m = ++a;
        add     DWORD PTR [rbp-4], 1
        mov     eax, DWORD PTR [rbp-4]
        mov     DWORD PTR [rbp-12], eax
        ; int n = ++b;
        mov     eax, DWORD PTR [rbp-8]
        lea     edx, [rax+1]
        mov     DWORD PTR [rbp-8], edx
        mov     DWORD PTR [rbp-16], eax
        ; código para regresar de la función
        mov     eax, 0
        pop     rbp
        ret


Cómo se puede ver con el postincremento hay una instrucción de más.

engel lex

cuidado con el vicio de la falsa optimizacion... el orden de los factores afecta, se pueden hacer operaciones extras...

https://godbolt.org

int main() {
   int a = 0;
   int b = 0;
   int n = b++;
   int m = ++a;
}


Código (asm) [Seleccionar]
 
       push    rbp
       mov     rbp, rsp
       ; int a = 0;
       mov     DWORD PTR [rbp-4], 0
       ; int b = 0;
       mov     DWORD PTR [rbp-8], 0
       ; int n = ++b;
       mov     eax, DWORD PTR [rbp-8]
       lea     edx, [rax+1]
       mov     DWORD PTR [rbp-8], edx
       mov     DWORD PTR [rbp-12], eax
       ; int m = ++a;
       add     DWORD PTR [rbp-4], 1
       mov     eax, DWORD PTR [rbp-4]
       mov     DWORD PTR [rbp-16], eax
       mov     eax, 0
       ; código para regresar de la función      
       pop     rbp
       ret



El problema con la sociedad actualmente radica en que todos creen que tienen el derecho de tener una opinión, y que esa opinión sea validada por todos, cuando lo correcto es que todos tengan derecho a una opinión, siempre y cuando esa opinión pueda ser ignorada, cuestionada, e incluso ser sujeta a burla, particularmente cuando no tiene sentido alguno.

RayR

#7
Como ya habrás visto, el problema se debe a que C++ no especifica(ba) un orden de evaluación. Que esa línea funcione o no, dependería del compilador e incluso en un mismo compilador, el nivel de optimización seleccionado o incluso el código concreto en el que se use, podría alterar el resultado, precisamente porque, al no estar definido el orden de evaluación, los compiladores pueden hacerlo como quieran. Sin embargo esto ya se subsanó en C++17, por lo que si compilas para este estándar, aquí:

Código (cpp) [Seleccionar]
           numbers[previousIndex+1] = numbers[previousIndex--];
tienes garantizado que primero se evaluará lo de la derecha (previousIndex--) y obtendrás resultados consistentes. Aún así, yo recomendaría que hagas el decremento en su propia línea, tanto porque así tu programa funcionará en cualquier versión de C++, como porque es menos ambiguo para el programador; no todos están familiarizados con C++17, y aunque lo estemos, se nos pueden olvidar las reglas de evaluación.

Lo del incremento/decremento quizás amerita algo más de detalle. Primero, en C y C++, los operadores de preincremento no son más eficientes que los post. La confusión viene debido a que, en el caso de los objetos (a diferencia de los tipos básicos como int, long, etc.), al implementar la sobrecarga de dichos operadores, en el postincremento/decremento se debe crear una copia del objeto, lo que es menos eficiente. Pero recordar que con los objetos no estamos invocando a los operadores de incremento/decremento, sino a funciones que sobrecargan esos operadores, que no es lo mismo. Sin embargo, si no nos interesan los detalles internos, basta decir que el preincremento es más eficiente únicamente en el caso de objetos. Para variables de tipos básicos, no hay diferencia.

En realidad, como curiosidad, y contrario a la engañosa creencia antes mencionada, en algunos casos el postincremento/postdecremento (con tipos básicos) en teoría podría ser más eficiente que el preincremento, ya que el preincremento genera una dependencia de datos que no existe con el post, por lo que en este último caso podría haber un mejo aprovechamiento de los pipelines del procesador. Eso sí, en la práctica yo nunca lo he visto, pero he leído de dos programadores que lo afirman.

Edito: como dice engel lex, cuidado con las falsas optimizaciones. No podemos simplemente contar las instrucciones generadas y sacar conclusiones. Hay muchos factores en juego y para poder evaluar correctamente el rendimiento hay que conocer con cierta profundidad la arquitectura para la que se está programando, así como los patrones de uso concretos del programa a evaluar. Suponer que menos instrucciones == mayor velocidad no tiene sentido. De hecho, hace unos días aquí en otro tema del foro hablé de una función en la cual modifiqué el ensamblador generado por el compilador. El resultado fue una función con 4 o 5 instrucciones adicionales, pero que es un 20% más rápida que la original. Y esto no es ningún caso aislado, de hecho es muy común.

K-YreX

Cita de: engel lex en  2 Octubre 2019, 15:47 PM
aqui hay un tema que se explicó con un poco de detalle el asunto relacionado a los incrementos/decrementos post/pre, esto es un problema comun a descubrir cuando se está aprendiendo

https://foro.elhacker.net/buscador-t483138.0.html

no es un asunto de java vs c++ es un asunto de nivel mas bajo...
Gracias por el enlace. No sabía nada de los sequence points. Lo de Java vs C++ lo decía porque aun suponiendo que cada lenguaje funciona con sus normas, pensaba que seguirían un mismo estándar de orden de procesamiento. Pero como explico más adelante parece que cada uno usa un orden diferente.

Cita de: RayR en  2 Octubre 2019, 19:33 PM
tienes garantizado que primero se evaluará lo de la derecha (previousIndex--) y obtendrás resultados consistentes. Aún así, yo recomendaría que hagas el decremento en su propia línea, tanto porque así tu programa funcionará en cualquier versión de C++, como porque es menos ambiguo para el programador; no todos están familiarizados con C++17, y aunque lo estemos, se nos pueden olvidar las reglas de evaluación.
Ya he visto que es mejor separar los incrementos/decrementos en líneas diferentes pero quiero terminar de entenderlos. He realizado otra prueba con:
Código (cpp) [Seleccionar]

int numeros[5] = {1,2,3,4,5};
int j = 2;
numeros[j+1] = -numeros[j--];

Y el resultado es: numeros = {1,2,-3, 4,5}. Por lo que saco que primero se obtiene el valor almacenado en <numeros[j]>, luego se decrementa <j> y después se calcula la posición de memoria del valor <numeros[j+1]>. Como el índice <j> se decrementa antes de calcular la posición de memoria pues la posición vuelve a ser la misma al sumarle 1 al índice. (Todo esto compilado con -std=c++17)

Sin embargo, en Java si probamos a hacer lo mismo:
Código (java) [Seleccionar]

int numeros[] = {1,2,3,4,5};
int j = 2;
numeros[j+1] = -numeros[j--];

El resultado es: numeros = {1,2,3,-3,5}. Por lo que aquí parece que antes de realizar el decremento ya tiene la posición de memoria donde lo va a guardar calculada y por eso el decremento no se junta con el +1 llegando de nuevo a la misma posición como ocurre en C++.

Estoy estudiando la precedencia de los operadores y por eso me ha sorprendido que el resultado fuera diferente teniendo en cuenta que los incrementos/decrementos tienen más preferencia que las asignaciones.
También he visto en algunos sitios que los operadores en postfijo tienen más preferencia que en prefijo... pero en otros sitios aparece como que tienen la misma prioridad. No sé si esto depende también del estándar.
Y la última pregunta es: en una sentencia con postincremento, el incremento se produce cuando ya se ha terminado de ejecutar la sentencia (línea completa) o justo después de usar el valor sin incrementar?
Código (cpp) [Seleccionar]

cout << "Todos tenemos un defecto, un error en nuestro código" << endl;

RayR

#9
El operador de postincremento/decremento sí tiene mayor prioridad que la asignación, pero ése no es el problema. Lo que aquí entra en juego es lo que pasa antes de la asignación. Si tenemos algo así (un ejemplo más sencillo, para simplificar):

Código (cpp) [Seleccionar]

numeros[n] = n++;


obviamente, antes de la asignación, se debe determinar qué es lo que vamos a modificar (operando izquierdo),y qué valor le vamos a dar (derecho), es decir, se deben evaluar ambos lados de la operación. La cuestión es ¿cuál se evalúa primero? Antes estaba indefinido, pero C++17 dice que el de la derecha. Ése es precisamente el cambio relevante aquí.

Cita de: YreX-DwX en  3 Octubre 2019, 21:37 PM
Y el resultado es: numeros = {1,2,-3, 4,5}. Por lo que saco que primero se obtiene el valor almacenado en <numeros[j]>, luego se decrementa <j> y después se calcula la posición de memoria del valor <numeros[j+1]>. Como el índice <j> se decrementa antes de calcular la posición de memoria pues la posición vuelve a ser la misma al sumarle 1 al índice. (Todo esto compilado con -std=c++17)

Correcto. Podemos distinguir dos partes en la evaluación de n++ (como la de cualquier expresión): el cálculo del valor de retorno de la expresión, y sus efectos colaterales. En el caso de n++, lo que retorna dicha expresión es el valor de que tiene n al momento de evaluarse, y el efecto es la modificación de la propia variable n. Las dos cosas deben ocurrir en ese orden. Por eso, cuando se evalúa la parte derecha de tu asignación, como j vale 2, queda -numeros[2]. Cuando se termina de evaluar la expresión completa de la derecha (incrementar j, tomar el valor del elemento 2 de numeros, y negarlo), se procede a evaluar la parte izquierda, y como j ahora vale 1, queda numeros[1+1].

Los operadores en posfijo tienen mayor prioridad.

Cita de: YreX-DwX
Y la última pregunta es: en una sentencia con postincremento, el incremento se produce cuando ya se ha terminado de ejecutar la sentencia (línea completa) o justo después de usar el valor sin incrementar?

Bueno, en realidad eso no se define por sentencias sino por lo que se conocía como sequence points, pero resulta que incluso ese término es algo impreciso en C++ y ha quedado en desuso desde hace tiempo. Hasta donde sé, lo que preguntas sigue estando sin especificar, pues el estándar dice que, a menos que se indique lo contrario, el orden de los efectos colaterales de las expresiones y subexpresiones se considera "no especificado", y que yo sepa, sólo señala lo que sucede en casos concretos (como cuando expresiones tipo n++ se usan como argumentos a funciones, o con el operador ternario) pero no de forma general. Por eso, incluso en C++17, esto da un resultado indefinido (reiterar que el cambio en C++17 referido en este post habla sólo del orden de evaluación de los dos "lados" de una asignación):

Código (cpp) [Seleccionar]
n = n++ + n;

De cualquier forma, si nos limitamos a usos correctos, es bastante irrelevante el orden. Lo mejor es, ante la duda, hacer el incremento en su propia línea; el compilador no va a generar código menos (o más) eficiente por ello.

Se me olvidaba, en Java obtienes otros resultados simplemente porque sus reglas son diferentes a las de C++. En Java sí se especifica lo que debe suceder, y ahí el orden es de izquierda a derecha y, si no recuerdo mal, los efectos colaterales suceden de forma inmediata. No hay realmente nada de especial en como lo hace C++. Es simplemente que este lenguaje (igual que C) siempre ha sido demasiado permisivo, y deja muchas cosas indefinidas o no especificadas, muchas veces en aras de la portabilidad o para permitir que cada implementación pueda ser lo más eficiente posible.