Vous êtes sur la page 1sur 11

1 Presentacin

Recordemos que el lenguaje C++ est compuesto por cinco tipos de elementos: palabrasclave; identificadores; constantes; operadores y signos de puntuacin ( 3). Con estos elementos se construyen las sentencias, cuya ejecucin es secuencial, excepto cuando aparecen elementos que modifican el flujo "natural" del programa. Son las sentencias de seleccin. Por ejemplo, if..else ( 4.10.2); las de iteracin ( 4.10.3), del tipo for..., o do... y las de salto ( 4.10.4) como return o break. Considerando las sentencias como unidades de ejecucin, resulta relativamente fcil a cualquier estudiante conocer con exactitud el orden que seguir la ejecucin del programa en sus diversas bifurcaciones e iteraciones, as como la naturaleza de las operaciones involucradas. Sin embargo, la facilidad no es tanta cuando se considera cuales son las operaciones que se llevan a cabo "dentro" de las sentencias, y su orden. El asunto es importante porque muchas veces el resultado, o incluso la aparicin de errores de ejecucin y/o compilacin, depende justamente de estos detalles. Con los agravantes de que muchas de ellas son "ocultas", en el sentido que son realizadas automticamente por el compilador sin que tengamos constancia de su ocurrencia, y de que se siguen procesos distintos en compilacin y en ejecucin (por supuesto todos ellos pueden dar lugar a errores). En el presente captulo incluiremos algunas observaciones respecto a este asunto al que, en mi opinin, no se suele prestar la debida atencin a pesar de que en ciertos casos, su desconocimiento puede perfectamente mandarnos a limpiar cochineras a Carolina del Norte ( 1 ). Naturalmente no nos referimos a expresiones sencillas del tipo y = 2+x; sino a expresiones ms complejas. Por ejemplo: String S = s1+c1.*c1.pachar2+'C';
2 Evaluacin de expresiones

En general, salvo que se relacionen con las mencionadas sentencias modificadoras del flujo, las palabras-clave sealan al compilador aspectos complementarios que no alteran el orden de ejecucin dentro de la propia sentencia. Este orden viene determinado por cuatro condicionantes: 1. Presencia de parntesis que obligan a un orden de evaluacin especfico. 2. Naturaleza de los operadores involucrados en la expresin (asociatividad). 3. Orden en que estn colocados (precedencia). 4. Providencias (impredecibles) del compilador relativas a la optimizacin del cdigo.

En cuanto al primero, aunque el parntesis es un signo de puntuacin ( 3.2.6), podra considerarse como el operador de precedencia ms alta. Si existen parntesis, el compilador los evala en primer lugar. El segundo es especialmente importante, porque como veremos a continuacin, es precisamente su naturaleza la que establece dos propiedades importantes de los operadores: la asociatividad y la precedencia (3 ). El punto tercero es influyente porque a igualdad de precedencia, unos operadores se ejecutan en el orden en que aparecen escritos en el cdigo (de izquierda a derecha), y en otros casos es al contrario (dependiendo de su asociatividad). A su vez el punto cuarto encierra decisiones que son dependientes de la plataforma. Se refieren a medidas del compilador tendentes a la optimizacin del cdigo de la sentencia, que resultan incontrolables para el programador a no ser que adopte medidas especficas. Estas medidas suelen consistir en no simplificar demasiado las expresiones, y obtener resultados intermedios, que solo son necesarios para obligar a una forma determinada de obtener el resultado.

2.1 La precedencia indica cual es el orden de ejecucin de los operadores cuando existen varios. Por ejemplo, en la expresin: a * b + c++; // L.1

la precedencia determina que se ejecutar primero el operador postincremento ++ sobre c. A continuacin se aplicar el operador de multiplicacin * entre a y b. Finalmente se aplicar el operador suma + entre los resultados anteriores. As pues, la expresin es equivale a: (a * b) + (c++); // L.2 Este orden "natural" del compilador no necesita parntesis de forma que las sentencias L1 y L2 producen el mismo resultado. Cualquier otro debe ser forzado especficamente mediante la utilizacin de los parntesis correspondientes. Por ejemplo: a * (b + c)++; // L.3

2.2 La asociatividad indica el orden de ejecucin cuando en una expresin existen diversos operadores de igual precedencia. Puede ser de dos tipos: izquierda () o derecha () [1]. Por ejemplo, la suma binaria + tiene asociatividad izquierda, lo que significa que en una expresin como: a + b + c + d; la ejecucin seguir el orden: (((a + b) + c) + d);

Los operadores unarios y el de asignacin (=), tienen asociatividad derecha (). Todos los dems la tienen izquierda (). En consecuencia, si @ representa un operador binario, ante una expresin como: a @ b @ c @ d; el orden de evaluacin es desde la izquierda hacia la derecha. Pero si la expresin es del tipo: (...) @ (...) @ (...) @ (...); el orden de evaluacin de los parntesis es indefinido. Aunque una vez obtenidos todos los resultados parciales, la computacin sigue el orden indicado en el punto anterior. Si existen parntesis anidados se procede desde dentro hacia fuera: (((.1.)@(.1.)2) @ (.2.));
3 Precedencia y asociatividad de los operadores C++

En C++ existen 18 categoras de precedencia (ver tabla adjunta), algunas de las cuales contienen solo un operador. El orden en la tabla muestra las precedencias en orden descendente (misma lnea igual precedencia). Se ha incluido un nmero indicativo, los nmeros ms bajos tiene mayor precedencia que los ms altos. Cuando existen operadores duplicados en la tabla, la primera ocurrencia se refiere al operador unitario, la segunda al binario (por ejemplo, los casos de +, * y &). Esto significa que en la gramtica del C++, cuando un operador tiene dos formas, unaria y binaria, la primera tiene mayor precedencia que la segunda. Nota: el parntesis es un signo de puntuacin y no se incluye (el que se muestra en la tabla es el operador de invocacin de funcin), pero podra considerarse como el operador de precedencia ms alta (precedencia -1). Precedencia 0 1 2 3 4 5 6 7 :: () ! .* * + << < [] ~ / <= -> + % ++ -++ Operador . -typeid & * sizeof new delete Asociatividad

Operador de resolucin de mbito

->* Seleccin de miembro Suma y resta binarias >

Operadores aritmticos: multiplicacin, divisin y mdulo

>> Manejo de bits: desplazamientos izquierdo y derecho

>= Operadores relacionales, mayor-que, menor-que, etc.

8 9 10 11 13 14 15 17

== & ^ | || ?: = ,

!=

Operadores relacionales igual y desigual

Manejo de bits: AND lgico Manejo de bits: XOR Manejo de bits: OR Operador O lgico Operador condicional ternario ^= |= <<= >>=

12 && Operador Y lgico

*= /= %= += -= &= 16 throw Operador de lanzamiento de excepcin Operador coma

Nota: Post-operador incremento y decremento unitario. decremento unitario.

Pre-operador incremento y

Observe que los operadores de bits & ^ | (lneas 8, 9 y 10) tienen una precedencia menor que la igualdad == y desigualdad != (lnea 7), lo que supone que expresiones de comprobacin de bits como: if ((x & MASK) == 0) {...} deben ser adecuadamente parentizadas para garantizar que los resultados sean correctos.
Ejemplos

++ptr->x; Debido a que la precedencia del preincremento es menor que la del selector indirecto de miembro, la expresin anterior equivale a: ++(ptr->x); El resultado es que se incrementa el miembro x, no el valor del puntero ptr.

*ptr->x; por lo mismo, esta indireccin equivale a *(ptr->x);

El resultado depende del tipo de miembro x. Por ejemplo, si x es un int, se obtendra un error de compilacin (no puede obtenerse la indireccin de un int). Por el contrario, si es un puntero, se obtendra el objeto sealado por l.
4 Orden de evaluacin

Las reglas anteriores no definen completamente el orden de evaluacin de las expresiones, de forma que dejan algunos aspectos a criterio del compilador (son dependientes de la plataforma y de las decisiones que esta quiera tomar tendentes a la optimizacin). Por ejemplo, en la expresin: (a + b) + ( c + d); es evidente que las subexpresiones (a + b) y (c + d) se resolvern antes que la suma central, pero no est definido cual de las dos se ejecutar en primer lugar [2]. El Estndar seala al efecto: "Con excepcin de los casos sealados, no est especificado el orden de evaluacin de los operandos de operadores individuales y subexpresiones de expresiones individuales, as como el orden en que ocurren los efectos laterales de las expresiones". En consecuencia, la mayora de compiladores C++ no garantizan el orden en que se evalan los operandos de una expresin (con excepcin de los operadores &&, ||, ?: y , ). Por lo general los compiladores tratan de arreglar las expresiones para mejorar la calidad general del cdigo, as que no puede especificarse el orden en que sern evaluados realmente los operandos a menos que algn operador lo establezca especficamente. Por esta razn hay que tener especial cuidado con expresiones en las que un valor sea modificado ms de una vez. Por lo general deben evitarse expresiones que simultneamente modifiquen y usen el valor de algn objeto. Por ejemplo, considere la expresin: i = v[i++]; donde i es indefinido cualquiera que sea su valor previo, porque su valor resultante depende de que sea incrementado antes o despus de la asignacin. De forma similar en: int total = 0; sum = (total = 3) + (++total); los valores sum y total son ambiguos (es sum = 4 sum = 7 ?). La solucin es redisear la expresin utilizando una variable temporal: int temp, total = 0; temp = ++total; sum = (total = 3) + temp; en cuyo caso la sintaxis fuerza un orden de evaluacin.

Ejemplo: int x = 5; cout << x++ + 5 << "; x = " << x << endl; x = 5; cout << ++x + 5 << "; x = " << x << endl; x = 5; cout << x-- - x++ << "; x = " << x << endl; x = 5; cout << x-- - ++x << "; x = " << x << endl; x = 5; cout << ++x - x-- << "; x = " << x << endl; Salidas: Borland C++ Builder 5.6.4 y 6.0 para Win32 GNU C++ 3.3.1-20030804-1 para Win32. 10; x = 5 11; x = 5 1; x = 5 0; x = 5 0; x = 5 10; x = 6 11; x = 6 0; x = 5 0; x = 5 0; x = 5

// L.1 // L.2 // L.3 // L.4 // L.5

Para entender los resultados es preciso considerar que los operadores incremento y decremento ++ y --, en sus versiones "pre" y "post", tienen mayor precedencia que las suma y resta binarias, por lo que se ejecutan primero. Pero los efectos laterales de estos ltimos (postincremento y postdecremento), solo tienen efecto despus que la expresin ha sido evaluada. Esto puede evidenciarse incluyendo parntesis en las expresiones anteriores y comprobando que se obtienen los mismos resultados: int x = 5; cout << (x++) + 5 << "; x = " << x << endl; x = 5; cout << ( ++x) + 5 << "; x = " << x << endl; x = 5; cout << (x--) - (x++) << "; x = " << x << endl; x = 5; cout << (x--) - ( ++x) << "; x = " << x << endl; x = 5; cout << ( ++x) - (x--) << "; x = " << x << endl;

En L.1, el valor 5 de x es sumado con la constante numrica 5, y el resultado es 10. Una vez que se ha completado la expresin, se produce el postincremento, con lo que x tiene el valor 6.

En L.2 el proceso es anlogo, con la salvedad de que ahora el incremento de x se realiza antes de que la expresin sea evaluada, con lo que al valor 6 de x se le suma la constante 5 y el resultado es 11. En ambos casos, el valor de x despus de ejecutadas las sentencias L.1 y L.2 es 6. La aparente contradiccin en los valores de x mostrados por ambos compiladores, se deben a que el compilador Borland evala las expresiones del tipo cout << A << B << endl; de derecha a izquierda, mientras que GNU lo hace de izquierda a derecha [4]. Que el valor de x es 6 en ambos casos, puede ser comprobado aadiendo una sentencia inmediatamente despus de L.1 y L.2: cout << "El valor de x es ahora " << x << endl; Aqu el resultado obtenido con ambos compiladores es 6. Observe que en las tres salidas restantes, el valor resultante de x es 5 cualquiera que sea el orden de ejecucin de la sentencia, ya que el incremento y decremento se anulan entre s. Las salidas L.4 y L.5 (en las que concuerdan ambos compiladores) son 0 porque, en ambos casos se efecta primero el preincremento. Al valor 6 de x se le resta el valor actual 6, con lo que el resultado es 0. Una vez evaluada la expresin, tienen efecto los resultados laterales. Es decir, el postdecremento de x, con lo que su valor vuelve a ser 5. La razn de la discrepancia de las salidas obtenidas en L.3, es de naturaleza anloga a la encontrada para los valores de x en las L.1 y L.2; la libertad que concede el Estndar respecto al orden de a la evaluacin de expresiones del tipo: (...) @ (...) @ (...) @ (...); En este caso no existe precedencia para decidir cual de las expresiones x++ o x-- se evaluar primero. Aparentemente el compilador Borland precede de derecha a izquierda y adems de forma errnea, dado que los resultados laterales solo deban manifestarse "despus" de evaluada la expresin. El resultado 0 de GNU es evidentemente el correcto. Al valor actual 5 de x, se le resta el valor actual (5) y el resultado es cero. A continuacin se producen el postdecremento y postincremento, cuyo resultado vuelve a ser 5 cualquiera que sea el orden de ejecucin adoptado. Nota: el "bug" de esta versin de Borland parece debido a que, en este caso, el compilador procede de derecha a izquierda, y el resultado (postincremento) tiene lugar "antes" que la resta, con lo que al valor resultante 6 de x, se le resta el (todava valor anterior 5). Esta teora y el error se confirman cor el resultado proporcionado por: x = 5; cout << x++ - x-- << "; x = " << x << endl; // -> -1; x = 5

4.1 El siguiente ejemplo ofrece una idea de los procesos involucrados en la evaluacin de expresiones. void func(int x, int y, int z) { // L1: ... int r = x + y + (z + 'A'); // L2: } Para ejecutar L2, el compilador intenta en primer lugar resolver la expresin del Rvalue, donde se encuentra con diversos identificadores y operadores. Antes que nada, durante la fase de compilacin, se realiza un "Name-lookup" ( 1.2.1) para averiguar "que cosa" son las etiquetas x, y, z (ya sabe que 'A' es una entidad tipo const char y que r es un int que se declara en la misma sentencia). A continuacin, suponiendo que ha encontrado en L1, que son tipos int, procede a codificar el resultado. Como el parntesis es el operador de mayor precedencia debe ejecutarlo en primer lugar, para lo cual intenta evaluar z + 'A'. Estamos ante un operador binario suma (+) con dos operandos: izquierdo y derecho. Aqu debemos sealar ( 4.9.18) que esta expresin es otra forma sintctica de la invocacin z.operator+('A'). Es la invocacin de un mtodo cuyo prototipo es: int int::operator+(int i); El mtodo operator+ de la clase de los enteros devuelve un entero y recibe un entero, as que el argumento actual 'A', un tipo const char, debe ser promovido al tipo del argumento formal, un tipo int ( 4.4.6). Suponiendo que esto sea posible, que lo es ( 2.2.5), se anotara el resultado (int) devuelto por la funcin en un objeto temporal (lo llamaremos T1) y se procedera con el resto de la expresin. Para resolver x + y + T1, en la que todos los operadores + tienen igual precedencia, el compilador procede segn su asociatividad, que en este caso es izquierda. En primer lugar, resuelve x + y; el nuevo parcial (lo llamaremos T2) es alojado en un objeto creado al efecto. Finalmente, se realiza la operacin T2 + T1 con un nuevo resultado parcial T3, y seguira la ejecucin del resto de la sentencia. En este caso, la expresin int r = T3; supone la invocacin del constructor-copia ( 4.11.2d4) de la clase int.

Supongamos ahora un caso exactamente anlogo al anterior en el que los objetos x, y y z no sean tipos bsicos sino tipos abstractos pertenecientes a una clase C diseada por el usuario: void func(C x, C y, C z) { // L1: ... C r = x + y + (z + 'A'); // L2: }

Suponemos que hemos definido adecuadamente el operador suma binaria para los miembros de la clase C y que disponemos de un conversor adecuado por el que el tipo const char puede ser convertido a tipo C, de forma que puede efectuarse la operacin z + 'A'. Por ejemplo, proporcionando un constructor-conversor en la clase ( 4.11.2d1). En este caso, las cosas funcionan de forma paralela a la descrita y el programa funciona sin dificultad [3]. Estamos tan contentos de lo bien que funciona nuestra clase, cuando un poco ms adelante se nos presenta una sentencia "muy" parecida a la anterior: C r2 = x + ('B' + y) + z; pero descubrimos con sorpresa que ahora el compilador lanza un extrao error: Illegal structure operation in.... Dependiendo de nuestro estado anmico ese da, podemos tardar ms o menos en descubrir que el problema radica precisamente en la computacin del parntesis (que aparentemente haba funcionado correctamente en otras sentencias). La cosa puede ser aun ms desconcertante si sabemos que el compilador dispone (y lo ha demostrado) de recursos para convertir en caso necesario un const char a C. Adems, en el contexto de la expresin resulta evidente que el resultado del parntesis "debe" ser un tipo C, y para ms INRI, el Rvalue (y) es tambin de este tipo... El problema es que (en el estado actual) el compilador no es an lo suficientemente inteligente. Al intentar evaluar la subexpresin 'B' + y no tiene en cuenta el contexto, e intenta encontrar una definicin de la asignacin que corresponda al prototipo: const char char::operator+(C c); Como evidentemente este mtodo no est definido para los tipos char, el compilador lanza una advertencia anunciando que no puede realizar la suma en el orden solicitado. Si no podemos alterar el orden de los sumandos (porque nuestra definicin de suma para operandos de tipo C no sea conmutativa) la nica posibilidad es forzar nosotros mismos la conversin, para lo que existen dos alternativas: C r = C('B'); // Ok. conversin implcita C r2 = x + (r + y) + z; // Opcin-1 C r2 = x + (static_cast<String>('B') + y) + z; // Opcin-2

4.2 A continuacin se muestra otro ejemplo funcional de los insidiosos errores que pueden cometerse C++ cuando se olvida algn "pequeo" detalle. Considere el sencillo programa adjunto y sus sorpresivos resultados:

#include <iostream> using namespace std;

int main() { // =============== int ai[] = {2, 3}; int z0 = ai[0] + ai[1]; // suma 2 + 3 cout << "Resultado z0 == " << z0 << endl; int* pt1 = ai; int z1 = *pt1; cout << "Resultado z1 == " << z1 << endl; int* pt2 = ai; int z2 = *(++pt2); cout << "Resultado z2 == " << z2 << endl; int* pt3 = ai; int z3 = *pt3 + *(++pt3); cout << "Resultado z3 == " << z3 << endl; int* pt4 = ai; int z4 = *(++pt4) + *pt4; cout << "Resultado z4 == " << z4 << endl; } Salida BC++ Builder 5.5, BC++ Builder 6, MS Visual C++ 6: Resultado z0 == 5 Resultado z1 == 2 Resultado z2 == 3 Resultado z3 == 6 Resultado z4 == 6 Salida Dev-C++ 4.9.9 GNU Compiler Collection G++ 3.3.1-20030804-1 Resultado z0 == 5 Resultado z1 == 2 Resultado z2 == 3 Resultado z3 == 5 Resultado z4 == 6
Comentario

Los resultados z0 a z2 son los esperados, sin embargo, z3 y z4 son aparentemente errneos. La primera explicacin plausible es suponer que, en este caso, la evaluacin empieza de derecha a izquierda, pero precisamente el resultado z4 parece desmentirlo. La segunda explicacin sera suponer un error en el compilador. Sin embargo, las salidas anteriores son exactamente las mismas con BC++ Builder 5.5 que con MS Visual C++ 6.0 (

7). Lo que ocurre realmente es que en ambos casos (z3 y z4), cualquiera que sea su posicin, derecha o izquierda, el compilador resuelve primero la expresin *(++pt3), y despus *pt3, con lo que al primer resultado (3) se le suma el valor de *pt3, pero entonces el puntero ya ha sido modificado, con lo que su valor es tambin 3. En cuanto a la salida del compilador GNU Cpp para Windows, parece evidente que su ejecucin, al menos en estas expresiones, es siempre de izquierda a derecha.

4.3 El lenguaje tampoco se garantiza el orden en que se evalan los argumentos de una funcin. Por ejemplo: printf ("%d %d\n", ++n, power(2, n)); // Incorrecto !! puede producir diferentes resultados con diferentes compiladores. La solucin es: ++n; printf ("%d %d\n", n, power(2, n));

4.4 Aunque hemos sealado que, para mejorar la calidad del cdigo, C++ reagrupa las expresiones reordenando los operadores asociativos y conmutativos con independencia de parntesis, en las expresiones con coma el valor no queda en ningn caso afectado por la reordenacin. Por ejemplo: sum = (i = 3, i++, i++); // OK: sum = 4, i = 5

5 Errores y desbordamientos (overflow)

Durante la evaluacin de una expresin pueden encontrarse situaciones problemticas, como divisin por cero o valores fraccionarios fuera de rango. El desbordamiento de enteros es ignorado (C usa una aritmtica de base 2n en registros de n-bits), pero los errores detectados por las funciones de las libreras matemticas pueden ser controlados mediante rutinas estndar o de usuario. Ver _matherr y signal.
6 Expresiones con coma

Las expresiones con coma son conjuntos de subexpresiones separadas por coma y agrupadas por parntesis. Cada subexpresin se evala una a continuacin de otra empezando por la izquierda, el resultado es el valor

Vous aimerez peut-être aussi