Hay un sutil hecho en los lenguajes de programación compilados que, aunque de sobra conocido en el mundo de la creación de compiladores, no lo es tanto por los programadores en general. Es la diferenciación entre constantes estáticas y constantes dinámicas.
Constantes estáticas
Imaginemos que en nuestro lenguaje de programación tenemos definida una función a la que queremos llamar con el argumento 3. Esto podría codificarse de la siguiente forma:
f(3)
El resultado de la compilación de la línea anterior es un código ejecutable que realiza dos acciones:
- Introducir el valor 3 en los argumentos de llamada
- Llamar al código de la función cuyo nombre es "f"
- Tomar el resultado de la llamada como resultado del programa
Es muy sencillo introducir ahora una constante que tenga el valor 3 y llamar a la misma función con esta constante.
const A = 3
f(A)
El resultado de la compilación de este nuevo programa no es más que el mismo código. El compilador, acertadamente, ha sustituido el símbolo "A" por el valor 3 antes de generar el código ejecutable (es lo que se llama propagación de constantes). Esto significa que el programa anterior es completamente equivalente al inicial.
Como todas las transformaciones que hacemos ocurren a la hora de compilar (de hecho el código ejecutable resultante es idéntico en ambos casos) a este tipo de constantes se las llama constantes estáticas.
Plegado de constantes
Imaginemos que ahora queremos realizar la siguiente llamada a la misma función "f".
f(1+2)
El compilador tiene ahora dos opciones. La primera es la generación de código ejecutable directa. Este código ejecutable sería algo como:
- Introduce el valor 1 en los argumentos de llamada (para la suma)
- Introduce el valor 2 en los argumentos de llamada (para la suma)
- Llamar al código de la función suma
- Introducir el resultado de la llamada en los argumentos de una nueva llamada (para "f")
- Llamar al código de la función cuyo nombre es "f"
- Tomar el resultado de la llamada como resultado del programa
La segunda opción que puede tomar el compilador es esforzarse antes de generar el código en resolver las operaciones que se realicen sobre constantes. En este caso primero sumaría 1+2 y obtendría el valor 3 y, luego, generaría el código.
- Introducir el valor 3 en los argumentos de llamada
- Llamar al código de la función cuyo nombre es "f"
- Tomar el resultado de la llamada como resultado del programa
A esta segunda opción se la llama plegado de constantes. El plegado de constantes no es más que ejecutar parte del código en el compilador en vez de en el programa. La razón de esto es muy sencilla. En el compilador se va a ejecutar una única vez mientras que en el programa se va a ejecutar muchas veces (al menos tantas como veces ejecutemos el programa).
De nuevo, como el plegado de constantes se efectúa durante la compilación, se dice que es una operación estática o en tiempo de compilación.
Evaluación de argumentos
Ejecutemos el siguiente código a continuación:
const A = 3
const B = A
f(B)
Nadie duda en este punto que el símbolo "B" estará asignado con el valor 3 ya que "A" se valuaba a 3. ¿Por qué no se asigna al símbolo "B" el valor "A" en vez del valor 3? Ciertamente, existen dos detalles importante:
- Lo que hay a la derecha (el argumento) del operador de asignación "=" se evalúa.
- Lo que hay a la izquierda (el símbolo) del operador de asignación "=" no se evalúa.
Aquí, cuando hablamos de evaluación, hablamos de evaluación en tiempo de compilación. Si nos fijamos bien, si hacemos f(1+2) también evaluamos el 1+2 antes de generar código. En general, se evalúan los argumentos de las funciones y no se evalúan los argumentos de las macros. De esto ya hablé anteriormente.
Otra vez, estamos hablando de transformaciones estáticas durante el tiempo de compilación.
Reserva de memoria
La diferencia de una constante con una variable es clara. Las variables pueden cambiar de valor durante la ejecución pero este valor es desconocido durante la compilación. Esto significa que hay que guardar una reserva de memoria durante la ejecución para almacenar el valor de la variable.
var A = lee_de_teclado()
El programa anterior generaría un código ejecutable como el que sigue:
- Llama al código de la función cuyo nombre es "lee_de_teclado"
- Guarda el resultado en la reserva de memoria para la variable "A"
Lo importante aquí es que no conocemos el valor que va a tener "A" hasta que no ejecutemos el programa y escribamos algo en el teclado. Por tanto, no estamos hablando de operaciones estáticas sino dinámicas que requieren la ejecución del programa.
Sin embargo, ¿qué pasaría si sí conociésemos el valor que va a tener "A" aunque sea una variable?
Constantes dinámicas
La pregunta anterior aparecería en el caso de este pequeño programa:
var A = 3
f(A)
Si el compilador no hiciera nada antes de generar código, el código ejecutable resultante sería:
- Guarda el valor 3 en la reserva de memoria de la variable "A"
- Introducir el valor que hay en la reserva de memoria de "A" en los argumentos de llamada
- Llamar al código de la función cuyo nombre es "f"
- Tomar el resultado de la llamada como resultado del programa
Obviamente, podríamos reducir el programa a f(3) pero que "A" sea una variable significa que puede ser modificada en tiempo de ejecución. Concretamente es el caso que sigue.
var A = 3
f(A)
A = lee_de_teclado()
Y lo que es peor, la modificación de "A" podría no producirse en la misma hebra. Podría ser concurrente por lo que no sabríamos si entre "var A = 3" y "f(A)" se habría modificado el valor de "A".
La solución a esto es prohibir que "A", aunque sea variable, pueda ser modificada. En nuestro lenguaje podríamos escribirlo así:
var const A = 3
//No permitimos que A se modifique aquí
f(A)
De esta forma se evita el problema anterior y el compilador es libre de optimizar. Debido a que este tipo de constantes requieren reserva de memoria durante la ejecución se denominan constantes dinámicas.
Utilidad de las constantes dinámicas
Entonces, ¿para qué sirve una constante dinámica? Porque si es constante podemos sustituirla y el programa anterior sigue siendo equivalente a f(3). Bien. Existen dos utilidades importantes.
La primera es que las constantes dinámicas están en memoria y podemos obtener su dirección de memoria. Las constantes estáticas sólo están en compilación y se olvidan cuando se genera el código ejecutable.
El que estén en memoria no es trivial, ya que esto nos permite crear objetos en memoria con una estructura compleja. Ejemplo de esto son arreglos o listas. En este tipo de estructuras es fundamental saber las direcciones de memoria ya que se usan punteros en su implementación.
La segunda utilidad está completamente ligada a la característica dinámica. Es posible tener una constante dinámica, cuyo valor el compilador desconoce, pero que sabe que no va a poder ser modificada. El código que sigue es un buen ejemplo de ello.
var const A = lee_de_teclado()
// Por seguro que nadie va a modificar A aquí
f(A)
Esta característica, también llamada inmutabilidad, es fundamental para la programación concurrente.
El origen de la confusión
La mayoría de los lenguajes de programación actuales no hacen una distinción entre estos dos tipos de constantes en el ámbito del compilador (no así en el del preprocesador).
Por ejemplo, en C++, las constantes son dinámicas casi siempre.
static const int A=3;
memset((void*)&A, 0, sizeof(A)); //OK. A es una constante dinámica.
Sin embargo, se pliegan.
sin(A+A); //Genera el mismo código que sin(6).
Existen mecanismos poco intuitivos y claros para forzar el uso de constantes estáticas.
enum {A=3};
memset((void*)&A, 0, sizeof(A)); //ERROR. A es una constante estática.
Por esta razón algunos opinan que haber elegido "const" como palabra clave fue un error y debería haberse usado "immutable". Java no soluciona el problema y usa variables "static final". En cambio, en C# (por fin) hay distinción entre "const" y "readonly".