Correctitud de constantes

adecuada declaración de variables u objetos como inmutables

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

editar

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»,[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

editar

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ó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

editar

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

editar

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

editar

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[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

editar

Existen 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

editar

El 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

editar
int[] 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

editar
class 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