En programación, la correctitud de constantes es el tipo de correctitud que hace referencia a la adecuada declaración de variables u objetos como inmutables. El término es mayormente usado en el contexto de C o C++, y recibe su nombre de la palabra reservada const de estos lenguajes.
El uso de la palabra reservada const indica lo que el programador «debe» hacer, no necesariamente lo que el programador «puede» hacer, pues calificar datos con esta palabra reservada no provoca que se almacenen en un tipo de memoria de sólo lectura, sino que ordena al compilador realizar verificaciones sobre el código en tiempo de compilación para finalizar con un error el proceso de compilación en el caso de intentar modificar un dato constante.
El hecho de que sea posible modificar datos calificados con const en tiempo de ejecución prueba que estos no se almacenan en memoria de sólo lectura. Para realizar estos cambios en tiempo de ejecución, deben evitarse las verificaciones que el compilador realiza sobre los valores const mediante el uso de conversiones de tipo o uniones.
const int constante = 0; // Valor constante
// Referencia no constante a un valor constante, sin usar la conversión const_cast sería un error int &noConstante = const_cast<int &>(constante); // Mediante la referencia no constante se modifica el valor constante: noConstante = 1;
// Puntero a no constante apuntando a un valor constante, sin usar la conversión const_cast sería un error int *punteroANoConstante = const_cast<int *>(&constante); // Mediante el puntero a no constante se modifica el valor constante: *punteroA_No_Constante = 2;
// Puntero a no constante apuntando a un valor constante, sin usar la conversión estilo C sería un error punteroANoConstante = (int *)&constante; // Mediante el puntero a no constante se modifica el valor constante: *punteroA_No_Constante = 3;
// Unión que contiene puntero a constante y puntero a NO constante unión constanteYMutable { const int *punteroAConstante; int *punteroA_No_Constante; } u;
// Correcto: Puntero a constante apuntando a un valor constante u.punteroAConstante = &constante; // Mediante el otro miembro de la unión se modifica el valor constante: *u.punteroA_No_Constante = 4;
Cabe destacar que el compilador puede decidir realizar optimizaciones sobre los valores calificados con const —como, por ejemplo, utilizar el valor literal en lugar del valor almacenado en la variable—. Esta optimización es conocida como propagación de constantes y también se aplica sobre los literales de texto const char *; dado que no es posible modificar el valor de un literal, el ejemplo anterior daría lugar a un comportamiento indefinido.
Los métodos no estáticos pueden declararse como const. Al hacerlo el puntero this dentro del método es de tipo valor_de_retorno const * const en lugar de valor_de_retorno * const. Esto significa que, dentro del método constante, el compilador tratará como error cualquier llamada a otros métodos no constantes o la modificación de cualquier campo del objeto.
En C++, un campo puede ser declarado como mutable, indicando que la anterior restricción no se aplica sobre él. En algunos casos, esto puede ser útil, por ejemplo, para cachear de datos, contar referencias o sincronizar datos. En estos casos, no se altera el estado lógico del objeto, pero no es físicamente constante porque su representación binaria puede cambiar.
Sintaxis de C y lenguajes derivados
En C y otros lenguajes derivados, todos los tipos de datos -incluyendo los definidos por el usuario— pueden ser declarados con const. Una correctitud de constantes dicta que todas las variables u objetos deben ser declarados constantes a no ser que exista intención de modificarlos. Este uso proactivo de const hace que los valores sean «más fáciles de entender, rastrear y razonar sobre ellos»,Herb Sutter y Andrei Alexandrescu (2005). C++ Coding Standards. p. 30. Boston: Addison Wesley. ISBN 0-321-11358-6 además de mejorar la legibilidad y comprensión del código, facilitando las tareas del equipo encargado del mantenimiento del código, ya que el propio código muestra la intención de uso de cada valor.
Tipos de datos simples
Para tipos de datos simples —excepto punteros—, el calificador const puede situarse en ambos lados del tipo —es decir, const char foo = 'a'; equivale a char const foo = 'a';—. En algunos compiladores, usar const en ambos lados del tipo —por ejemplo, const char const— genera una advertencia pero no un error.
La flexibilidad para situar el calificador const existe por razones históricasPreguntas frecuentes sobre C++ por Bjarne Stroustrup http://www.stroustrup.com/bs_faq2.html#constplacement —el C y C++ previos a la estandarización imponían pocas, o ninguna regla de ordenación en los calificadores de tipo—. Dado que usar el calificador antes o después del tipo no provoca ambigüedades, se decidió no desarrollar una regla de ordenación para este caso.
Punteros y referencias
El significado de const es más complicado para punteros y referencias —tanto el puntero como el dato apuntado, incluso ambos, pueden ser calificados con const—. Además, la sintaxis puede ser confusa. Pueden declararse punteros constantes que apunten a datos no constantes, o punteros modificables que apunten a datos no modificables, o punteros constantes a valores constantes. No se puede cambiar el dato al que apunta un puntero constante, pero puede modificarse el dato apuntado. Por el contrario, puede modificarse el dato al que apunta un puntero constante —que debe ser un dato del mismo tipo o un tipo convertible al del puntero—, pero no puede modificarse el valor apuntado a través del mismo. Finalmente, puede declararse un puntero constante a un dato constante, pero no puede cambiarse el dato al que apunta ni modificar el valor del dato apuntado a través del mismo. El siguiente ejemplo ilustra estos matices:
void Foo( int * puntero, int const * punteroAConst, int * const punteroConstante, int const * const punteroConstanteAConst ) { *puntero = 0; // Correcto: modifica el dato apuntado puntero = NULL; // Correcto: modifica el puntero
*punteroAConst = 0; // Error! No puede modificarse el objeto apuntado punteroAConst = NULL; // Correcto: modifica el puntero
*punteroConstante = 0; // Correcto: modifica el dato apuntado punteroConstante = NULL; // Error! No puede modificarse el puntero
*punteroConstanteAConst = 0; // Error! No puede modificarse el objeto apuntado punteroConstanteAConst = NULL; // Error! No puede modificarse el puntero }
Según las convenciones de C para declaraciones, la declaración sigue al uso, por lo que escribir * sobre un puntero indica su desreferenciación. Así pues, en la declaración int *puntero, al desreferenciar el puntero *puntero se obtiene int, mientras que puntero es un puntero a int. En consecuencia, const modifica el identificador a su derecha.
Sin embargo, las convenciones de C++ asocian el * con el tipo, como en int* puntero y const modifica el tipo a la izquierda. int const * punteroAConstante puede leerse como *punteroAConstante es un int const —el valor es constante—, o punteroAConstante es un int const * —el puntero apunta a un entero constante—:
int *puntero; // *puntero es un valor int int const *punteroAConst; // *punteroAConst es constante int * const punteroConstante; // punteroConstante es constante
int const * const punteroConstanteAConst; // punteroConstanteAConst es constante (puntero) // y *punteroConstanteAConst es constante (valor)
Según la convención de C++ de analizar el tipo, no el valor, la regla general es leer la declaración de derecha a izquierda. Así pues, todo lo que quede a la izquierda del asterisco pertenece al tipo apuntado y aquello a la derecha del mismo pertenece a las propiedades del puntero. En el anterior ejemplo, int const * puede ser interpretado como un puntero de lectura-escritura que apunta a un entero de sólo lectura.
También está permitido situar const a la izquierda del tipo en C y C++, como en la siguiente sintaxis:
const int* punteroAConst; // idéntico a: int const * punteroAConst const int* const punteroConstanteAConst; // idéntico a: int const * const punteroConstanteAConst Esta notación separa con mayor claridad las dos localizaciones de const, y permite que el * siempre se asocie a su tipo precedente, aunque aún necesite ser leído de izquierda a derecha: int* puntero; const int* punteroAConst; // (const int)*, no constante (int*) int* const punteroConstante; const int* const punteroConstanteAConst;
Las referencias de C++ siguen normas similares. Declarar una referencia const es redundante, ya que no es posible hacer que una referencia apunte a otro objeto:
int i = 22; int const & referenciaAConst = i; // Correcto int & const referenciaConstante = i; // Error, el "const" es redundante
Se pueden obtener declaraciones realmente complicadas usando vectores multidimensionales y referencias —o punteros— a punteros; sin embargo, estos usos se consideran confusos y propensos a errores; en consecuencia, se aconseja evitarlos o reemplazarlos por estructuras de mayor nivel de abstracción.
Parámetros y variables
const puede ser usado tanto en parámetros de función como en variables estáticas o automáticas, incluyendo globales o locales. La interpretación varía entre usos. Una variable const estática —variable global o variable estática local— es una constante, y debe usarse para representar datos como constantes matemáticas —por ejemplo, const double PI = 3.14159— o parámetros generales en tiempo de compilación. Una variable const automática —variable local no estática— debe ser inicializada, aunque puede usarse un valor diferente en cada llamada —por ejemplo, const int x_al_cuadrado = x*x;—. Un parámetro pasado como referencia const indica que el valor referenciado no se modificará, pues forma parte del contrato. Por ello, se suele favorecer el uso de const en parámetros pasados por referencia, pero no en los parámetros pasados por valor.
Métodos
Para aprovechar el contrato en tipos definidos por el usuario —estructuras y clases—, que pueden tener tanto métodos como campos, el programador debe marcar los métodos de instancia como const si no tiene intención de modificar los campos del objeto. Usar el calificador const sobre los métodos de instancia que lo requieran es esencial para cumplir con la correctitud de constantes, aunque este tipo de correctitud no esté disponible en otros lenguajes de programación orientada a objetos, como Java y C#, en la CLI de Microsoft, o en las Managed Extensions for C++.
Los métodos calificados con const pueden ser llamados desde objetos constantes y no constantes indistintamente, mientras que los métodos sin calificación const sólo pueden llamarse desde objetos no constantes.
El calificador const aplicado sobre un método de instancia afecta al objeto apuntado por el puntero this, que es un argumento implícito pasado a todos los métodos de instancia. Por lo tanto, los métodos constantes son la manera de aplicar la correctitud de constantes al parámetro implícito this. Por ejemplo:
class C { int i; public: int Get() const // Calificado con "const" { return i; } void Set(int j) // Sin calificación "const" { i = j; }
void Foo(C& noConstante, const C& constante) { int y = noConstante.Get(); // Correcto int x = constante.Get(); // Correcto: Get() es const
noConstante.Set(10); // Correcto: noConstante es modificable constante.Set(10); // Error! Set() no es const mientras que constante es un objeto const }
En el ejemplo anterior, el puntero implícito this del método Set() tiene el tipo C *const; mientras que el this de Get() es de tipo const C *const, lo cual indica que ese método no puede modificar el objeto mediante el puntero this.
Es posible declarar un método con el mismo nombre pero con diferente calificación const —y, posiblemente, diferente uso— para adaptarse a ambos tipos de llamadas. Por ejemplo:
class MiVector { int data; public: