Correctitud de constantes
En programación, la correctitud de constantes (del inglés: const correctness) 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
editarEn 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»,[1] 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
editarPara 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óricas[2] —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
editarEl 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
editarconst
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
editarPara 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[100];
public:
int & Get(int i) { return data[i]; }
int const & Get(int i) const { return data[i]; }
};
void Foo( MiVector & vectorNoConstante, MiVector const & vectorConstante )
{
// Obtenemos una referencia a un elemento del vector
// y modificamos su valor:
vectorNoConstante.Get( 5 ) = 42; // Correcto! (llama int & MiVector::Get(int))
vectorConstante.Get( 5 ) = 42; // Error! (llama int const & MiVector::Get(int) const)
}
La calificación const
del objeto determina qué versión de MiVector::Get()
será llamada, y si se obtendrá una referencia que pueda ser modificada o de sólo lectura.
Técnicamente, ambos métodos tienen diferentes firmas, ya que sus punteros this
tienen diferentes tipos. Esto permite al compilador escoger el método correcto.
Excepciones a la correctitud de constantes
editarExisten varias excepciones a la correctitud de constantes puras en C y C++, principalmente, por retrocompatibilidad de código.
La primera excepción, tan sólo aplicable a C++, es el uso de const_cast
, que permite eliminar la calificación const
de un dato, haciéndolo modificable. La necesidad de eliminar la calificación const
surge del uso de código heredado, o de bibliotecas que no pueden cambiarse pero que no cumplen con la correctitud de constantes. Por ejemplo:
// Prototipo de una función que no podemos modificar pero que
// sabemos que no modifica los datos apuntados por el puntero.
void LibraryFunc(int *ptr, int size);
void CallLibraryFunc(int const *ptr, int size)
{
LibraryFunc(ptr, size); // Error! ptr es constante
int *nonConstPtr = const_cast<int*>(ptr); // se elimina la calificación const
LibraryFunc(nonConstPtr, size); // Correcto
}
Sin embargo, el estándar ISO de C++ indica que intentar modificar un objeto calificado con const
a través de const cast
desemboca en un comportamiento no definido.
En el ejemplo anterior, si el puntero ptr
apunta a una variable global, local, o campo calificado con const
, o a un objeto creado dinámicamente mediante new const int
, el código sólo será correcto si la función LibraryFunc
realmente no modifica el valor apuntado por ptr
.
Existe una excepción[3] que se aplica tanto a C como a C++: los campos puntero o referencia «ignoran» la calificación const
de sus propietarios —es decir, en un objeto constante, todos sus miembros son constantes, excepto en el caso de punteros y referencias—. Considérese el siguiente código:
struct estructura
{
int valor;
int *puntero;
};
void Foo(const estructura & e)
{
int i = 42;
e.valor = i; // Error: e es constante, ergo valor es un entero constante
e.puntero = &i; // Error: e es constante, ergo puntero es un puntero constante a un entero
*e.puntero = i; // Correcto: el dato apuntado por puntero no es constante,
// aunque esto a veces no sea deseable
}
Aunque el objeto e
pasado a Foo()
sea constante, lo que implica que todos sus campos sean constantes, el dato apuntado por e.puntero
puede ser modificado pese a que no es adecuado desde el punto de vista de la correctitud de constantes —podría darse el caso de que e
fuese propietario del dato apuntado—.
Por este motivo, se ha argumentado que la calificación por defecto para punteros y referencias miembro debe ser una correctitud de constantes más profunda, pudiendo modificar el comportamiento por defecto con el calificador mutable
cuando el dato apuntado no pertenezca al objeto contenedor, pero aplicar este cambio crearía problemas de compatibilidad con código existente. Aun así, esta excepción permanece en C y C++.
La anterior excepción puede ser corregida usando una clase que oculte el puntero tras una interfaz que cumpla con la correctitud de constantes; no obstante, estas clases tampoco soportan las semánticas habituales de copia desde un objeto calificado con const
—lo que implica que la clase contenedora tampoco puede ser copiada con las semánticas de copia habituales—, ni permiten que otras excepciones ignoren la correctitud de constantes mediante copia involuntaria o intencionada.
Finalmente, varias funciones en la biblioteca estándar de C no cumplen con la correctitud de constantes, dado que reciben punteros const
a cadenas de caracteres y devuelven un puntero no-const
a una parte de la misma cadena.
Algunas implementaciones de la biblioteca C++ estándar, como la de Microsoft,[4] intentan corregir estas excepciones proporcionando dos versiones de algunas funciones: una versión con const
, y otra sin ello.
Correctitud de datos volátiles
editarEl cualificador volatile
de C y C++ indica que un objeto puede ser cambiado externamente en cualquier momento por medios ajenos al programa y, por lo tanto, debe volverse a leer de la memoria cuando se accede a él.
Este calificador se suele encontrar en código que manipula hardware directamente —en sistemas embebidos y controladores de dispositivos— y en aplicaciones multihilo.
volatile
se puede usar de la misma manera que const
en declaraciones de variables, punteros, referencias y métodos. De hecho, volatile
es usado en ocasiones para implementar estrategias de diseño por contrato que Andrei Alexandrescu denomina «correctitud de volátiles»,[5] aunque es mucho menos común que la correctitud de constantes. El calificador volatile
también puede ser eliminado mediante const_cast
, y puede combinarse con el calificador const
como puede verse en este ejemplo:
// Configura una referencia a un registro de hardware de solo lectura
// que está situado en una posición de memoria determinada.
const volatile int & registroDeHardware = *reinterpret_cast<int*>(0x8000);
int valorActual = registroDeHardware; // Leemos la memoria
int nuevoValor = registroDeHardware; // Leemos de nuevo
registroDeHardware = 5; // Error! No se puede escribir sobre una variable const
Dado que registroDeHardware
es volatile
, aunque el programador no pueda modificar su valor, no existen garantías de que mantenga los mismos datos en lecturas sucesivas. Las semánticas de esta variable indican que es de sólo lectura pero no inmutable.
const
e immutable
en D
editar
En la segunda versión del lenguaje D, existen dos palabras reservadas relacionadas con la correctitud de constantes:[6]
- la palabra reservada
immutable
denota un dato que no puede ser modificado a través de ninguna referencia; - la palabra reservada
const
se refiere a una referencia inmutable de un dato mutable.
Al contrario que el const
de C++, const
e immutable
de D son transitivos, y cualquier dato alcanzable a través de const
o immutable
es const
o immutable
, respectivamente.
Ejemplo de const e immutable en D
editarint[] foo = new int[5]; // foo es mutable.
const int[] bar = foo; // bar apunta a foo de manera inmutable.
immutable int[] baz = foo; // Error: las referencias a datos inmutables deben ser inmutables.
immutable int[] nums = new immutable(int)[5]; // No se pueden crear referencias mutables a nums
const int[] constNums = nums; // Correcto. immutable se puede convertir implicitamente a const.
int[] mutableNums = nums; // Error: No se puede crear una referencia mutable a un dato inmutable.
Ejemplo de const transitivo en D
editarclass Foo {
Foo next;
int num;
}
immutable Foo foo = new immutable(Foo);
foo.next.num = 5; // Error. foo.next es del tipo immutable(Foo).
// foo.next.num es del tipo immutable(int).
final
en Java
editar
En Java, el calificador final
indica que el campo o variable no es asignable. Por ejemplo:
final int i = 3;
i = 4; // Error! No se puede modificar un objeto "final"
El compilador debe ser capaz de decidir dónde inicializar las variables calificadas con final
, y debe ser inicializada una única vez o la clase no debe compilar. El final
de Java y el const
de C++ tienen el mismo significado al aplicarse sobre tipos básicos.
const int i = 3; // declaración C++
i = 4; // Error!
Respecto a los punteros, una referencia final
en Java tiene un significado similar a un puntero constante en C++.
Foo *const bar = direccion_de_memoria; // puntero constante
En el ejemplo anterior, bar
debe ser inicializado en el momento de la declaración y no puede cambiar su valor en adelante, pero el valor al que apunta sí es modificable. Es decir, *bar = valor
es válido, solo que no puede cambiar el lugar al que apunta. Las referencias finales de Java funcionan de la misma manera, con la particularidad de que no es necesario inicializarlas.
final Foo i; // una declaración Java
Nótese que Java no usa punteros.[7]
También se puede declarar un puntero a datos de sólo lectura en C++.
const Foo *bar;
En el ejemplo anterior, bar
puede modificarse para que apunte a cualquier dato de tipo Foo
; sin embargo el valor apuntado no podrá ser modificado mediante el puntero bar
. No existe un mecanismo equivalente en Java. Así pues, tampoco existen métodos const
.
No se puede forzar la correctitud de constantes en Java. Sin embargo, definiendo interfaces de sólo lectura a una clase se garantiza que los objetos puedan ser utilizados sin posibilidad de modificarlos. El framework de colecciones de Java proporciona mecanismos para crear un adapador sobre una Collection
mediante Collections.unmodifiableCollection()
y métodos similares.
Los métodos en Java pueden ser declarados como final
, pero este tipo de declaración no tiene relación con la correctitud de constantes sino con la herencia —significa que el método no puede ser sobreescrito en clases derivadas—.
El lenguaje Java marca const
como palabra reservada —por lo que no puede usarse como nombre de variable—, pero no le asocia semántica alguna. Esta palabra clave fue reservada para desarrollar una extensión del lenguaje Java que incluyese métodos const
y punteros a tipos const
al estilo C++[cita requerida]. Existe una petición para implementar correctitud de constantes en el Proceso de la Comunidad Java, pero fue cerrada en 2005 indicando que es imposible implementar dicho cambio de manera que sea retrocompatible.[8]
Código const
y readonly
en C#
editar
En lenguaje C#, el calificador readonly
tiene el mismo efecto sobre campos que el calificador final
de Java y el calificador const
de C++; el calificador const
en C# tiene un efecto similar al #define
de C++. El efecto inhibidor de herencia que Java aplica con el calificador final
sobre métodos es equivalente al calificador sealed
de C#.
El contrario que C++, C# no permite calificar métodos y parámetros con const
. Sin embargo, también se pueden utilizar parámetros de sólo lectura; el .NET Framework proporciona soporte para convertir colecciones modificables en colecciones inmutables que pueden ser pasadas a métodos y funciones.
Referencias
editar- ↑ Herb Sutter y Andrei Alexandrescu (2005). C++ Coding Standards. p. 30. Boston: Addison Wesley. ISBN 0-321-11358-6
- ↑ Preguntas frecuentes sobre C++ por Bjarne Stroustrup http://www.stroustrup.com/bs_faq2.html#constplacement
- ↑ «Standard C++». isocpp.org. Consultado el 27 de noviembre de 2019.
- ↑ documentación de strchr de Microsoft
- ↑ "Generic<Programming>: volatile — Multithreaded Programmer's Best Friend Volatile-Correctness or How to Have Your Compiler Detect Race Conditions for You" por Andrei Alexandrescu en C/C++ Users Journal Foro de expertos C++
- ↑ http://www.digitalmars.com/d/2.0/const-faq.html#const
- ↑ No More Pointers in Java
- ↑ Ticket for adding const parameters in Java