You are currently browsing the tag archive for the ‘const-correctness’ tag.

Creo que la palabra clave const aumenta bastante las emociones que uno podrá tener sobre C++: cuando es indudablemente un elemento para elevar el estilo de programación, también es uno que puede complicar bastante la compilación. En este artículo ponemos el enfoque más al por qué de usar entidades constante y como implementarlos en programas reales. He descrito en otro artículo de como y donde se puede usar la palabra clave const.

El compilador puede comprobar si un programa intenta modificar un valor constante. Declarar objetos constantes es, por lo tanto, una ayuda para detectar posibles errores en el código. Como es preferible detectar errores durante el tiempo de compilación – ya que el compilador hace el trabajo de encontrarlos – es en general preferible de programar código que cumple lo que se llama “const-correctness“. Además, la palabra const indica al lector del código que el programador intentó sólo leer de un valor. Sin duda, usar objetos constantes incrementa considerablemente la calidad y el estilo del código.

También es cierto que un programa funciona sin constantes. Por eso hay tendencia de no usarlas. Lo gordo viene cuando uno empieza a usarlas en un código que en general no lo usa. Esto puede causar errores de compilación hasta en la n-ésima llamada anidada a una función, que por alguna razón requiere que un valor no sea constante – aunque sólo lo lea y no lo escribe. Más aún fascinan los errores de la STL (Standard Template Library), que puede causar mensajes de error kilométricos que realmente quieren decir algo como “No puedo convertir el tipo const_iterator en iterator.” Como caramelito adicional hay métodos que sólo existen para un tipo de iterador. Por ejemplo, el operador [] del contenedor mapa sólo acepta un iterador a un mapa modificable.

¿Cómo podemos poner orden en el caos? Pues, básicamente en usar wrappers. En nuestro código usamos constantes de forma correcta. Si tenemos que llamar a una función que no podemos o queremos modificar, entonces convertimos los datos de la constante a una variable. El caso más simple sería una simple asignación.

const int constante = 5;
int parametro = constante;
funcion_sin_const_correctness(parametro);

Si no podemos copiar el valor, entonces podemos usar dos técnicas. Una es la de C mediante punteros.

const int constante = 5;
int& parametro_referencia = *(int*)(void*)(&constante);

La otra es con la expresión de C++ const_cast<TIPO>:

const int constante = 5;
int& parametro_referencia = const_cast<int>(constante);

Esta construcción está en el estándar explícitamente para quitar el const. Por lo tanto es la más comprensible a la hora de hacerlo. Aunque el const_cast se parece visualmente a un patrón (template), no lo es. Es una expresión nativa de C++.

Toma nota que no he puesto una copia de la constante al parámetro sino el parámetro es una referencia al valor. Esto puede ser preferible porque ahorra la llamada a un constructor de copia, pero también incrementa el riesgo que un programa erróneo escribe en la constante.

Lo más complicado suele ser convertir los iteradores de la librería estándar de C++, ya que un const_iterator no es lo mismo que un iterator declarado const. Son dos clases distintas que pueden tener diferencias internas importantes. En este caso no suele quedar otra que hacer apaños.

  • Podemos tener la suerte que en nuestra distribución de la librería estándar se puede construir un iterator con un const_iterator. Esto sería lo más fácil.
  • Podemos averiguar si continuamos con un puntero al objeto en lugar del iterador. Es decir algo como
    Contenedor<int>::const_iterator iterador_constante;
    int* puntero_parametro = &*iterador_constante;
    

    Podemos aprovechar que los punteros también son iteradores – al menos para contenedores de acceso aleatorio como vector y deque y arrays de C.

  • Es posible también que sólo necesitamos el valor del elemento que podemos entonces convertir como los variables y objetos constantes mencionados más arriba.
  • Si tenemos un bucle podemos estudiar si la variable del bucle será un iterator en lugar de un const_iterator.

Aún así puede haber el caso en que no podemos hacer nada más que abandonar la const-correctness. Esto sucede especialmente cuando hemos pasado un const_iterator como parámetro de función. En este caso nos consolamos en haber hecho lo máximo posible aunque no era lo máximo deseable.

En general es preferible usar const porque ayuda a mejorar el estilo del código y reduce errores a la hora ejecución. En la vida real nos podemos enfrentar a situaciones difíciles cuando actualizamos código viejo. Sin embargo, podemos intentar adaptar el mejor estilo posible en cada momento. No suele ser ventajoso mejorar el estilo de todo el código de golpe. Pero si lo mejoramos gradualmente en las partes que estamos tocando, acabamos tener actualizaciones de calidad sin necesidad de tocar código que ya funciona aunque tenga peor estilo.

Anuncios

En este artículo nos dedicamos como podemos usar constantes en C++. Hay otro artículo que trata de por qué conviene usar const.

A primera vista sorprende cuantas cosas pueden se constantes en C++. Se pueden declarar const

  • variables,
  • punteros o las variables a que apuntan (doblemente const),
  • referencias,
  • clases,
  • instancias de clases o
  • sólo miembros de datos dentro de una clase y también
  • métodos de una clase.

Para complicar la cosa aún más se puede usar const para sustituir enum y #define.

Variables constantes

Una variable declarada const no se puede cambiar, es decir, es una constante. No obstante, la palabra const sólo se tiene en cuenta durante el tiempo de compilación. El compilador emite un error cuando encuentra una instrucción que quiere asignar un valor a una constante, pero físicamente el valor de una constante puede estar guardado en la memoria RAM y cuesta poco asignar otro valor a esta posición mediante un uso malintencionado de punteros y casts.

Cuando un compilador optimiza el código ejecutable, entonces puede optar por no leer el valor de la memoria sino incrustarlo directamente en las instrucciones del procesador. Así es posible que el valor en la memoria RAM cambie pero el programa sigue con el valor que el compilador determinó durante la compilación. No obstante, la dirección de la constante sigue siendo válida aunque el ejecutable no lee de ahí.

Tener una representación física en la memoria es lo que distingue una variable declarada const de constantes definidas por enum o #define. Es decir, puedo hacer esto

const int c = 5;
const int* p = &c;  // p apunta a la dirección de c. *p vale 5.

pero no puede hacer esto

enum { CONSTANTE_ENUM };
const enum* pe = &CONSTANTE_ENUM // Error de compilación

Esto es porque una constante de enum sola no tiene tipo sino sólo representa un valor. Lo que convierte un enum en un tipo es el conjunto de los valores de que está formado.

La cosa es aún más complicada con los #define. Es una directiva del preprocesador que reemplaza un texto por otro antes de que el código llegue al compilador. Quiere decir, los nombres de las macros definido por #define no existen para el compilador.

#define CONSTANTE 5
int a = CONSTANTE;  // El compilador ve "int a = 5".

Punteros

Los punteros son variables que guardan una dirección de memoria. Como cualquier variable su valor puede ser constante o no. Los punteros son especiales por no tener sólo un tipo de qué son – una dirección de memoria – pero por tener también otro tipo asociado del valor a que apuntan. Y este valor puede ser constante o variable de forma independiente. Por este motivo puede haber dos const en la definición de un puntero.

      int        variable = 1;
const int        constante = 2;
const int *      puntero_a_constante = &constante;
      int *const puntero_constante_a_variable = &variable;
const int *const puntero_constante_a_constante = &constante;
  • El puntero_a_constante puede apuntar a varios objetos, pero no puede modificarlos. Cadenas de caracteres se definen típicamente así: como const char*.
  • Un puntero_constante_a_variable puede modificar el objeto a que apunta, pero no puede apuntar a otra cosa.
  • Finalmente, un puntero_constante_a_constante ni puede modificar el objeto a que apunta ni apuntar a otro objeto. Por este punto de vista una cadena de texto hardcoded tiene realmente el tipo const char *const. Sin embargo, se usa poco.

Como nota quiero decir también que existe la notación

      int const* puntero

pero se usa menos. De hecho no conviene usarlo porque es más ambiguo: ¿el const se refiere al tipo (int) o a la dirección de memoria (el “*”)?

Referencias

Para las referencias valen básicamente las mismas reglas que para punteros. No obstante hay una importante diferencia: referencias no pueden referirse a otra instancia. Al contrario de punteros las referencias no pueden existir por si mismas sino deben inicializarse a la hora de ser declarada.

      int        variable = 1;
const int        constante = 2;
const int &      referencia_a_constante = constante;

// &const es permitido pero innecesario. Por eso nunca se usa.
      int &const referencia_constante_a_variable = variable;
const int &const referencia_constante_a_constante = constante;

Como las referencias apuntan al mismo objeto durante toda su vida, & y &const es lo mismo, y por eso nunca se escribe &const.

Es fácil hacer un cast para convertir un puntero de un objeto a otro objeto con un tipo diferente. Las referencias, en cambio, tienen el mismo tipo que el objeto a que se refieren y aportan más seguridad contra un cast implícito.  Por eso es preferible usar referencias en lugar de punteros siempre cuando sea posible.

Muy especialmente conviene usar referencias a constantes en lugar de constantes como parámetros de funciones. Se usan igual pero sólo requieren copiar un puntero en lugar de un objeto entero. Es decir, no se llamará un constructor para una referencia.

Clases constantes

Igual como se puede convertir un tipo simple como int a constante, se puede declarar const a una estructura compleja. La construcción const MiClase convierte todos los miembros de esta instancia en constantes. Sólo se les pueden asignar un valor en el constructor y después ya no. Se puede forzar una excepción para un campo en la clase declarándolo mutable, pero en general hay poca razón de hacerlo.

También es posible declarar campos individuales constantes dentro de una clase. Por ejemplo, puedo crear una clase que guarde una posición como una posición inicial constante y un desplazamiento variable.

class MiSitio
{
    double desplazamiento;          // una variable
    const double posicion_inicial;  // una constante
};

El valor de posicion_inicial sería asignado en el constructor de la clase y se quedaría inmodificable durante la vida de la instancia. No obstante, cada instancia puede tener otro valor para posicion_inicial. Por lo tanto un miembro constante no es los mismo que una constante global y común a todas las instancias.

Lo que queda por hacer constante son los métodos de una clase. Métodos constantes “prometen” de no modificar ningún dato dentro de la clase. (Más correctamente ninguno que no sea mutable.) Son los únicos métodos que puedo llamar para una instancia constante.

class MiCosa
{
public:
    int mi_variable;

    void mi_metodo_constante() const
    {
        mi_variable = 0;  // Error de compilación.
        // No debo modificar campos en un método constante.
    };

    void mi_metodo_variable()
    {
        mi_variable = 0;  // Ok
    };
};

const MiCosa cosa;
cosa.mi_metodo_constante();  // Ok
cosa.mi_metodo_variable();   // Error de compilación.
// No puedo llamar a un método NO constante para un objeto constante.

Para complicar la cosa aún más, se pueden definir dos métodos iguales en que una es constante y la otra no. Es algo que usa mucho para los métodos que devuelven iteradores de inicio y final en los contenedores de la STL.

iterator begin(void);
const_iterator begin(void) const;

El método constante devuelve un iterador (puntero) a un objeto constante, mientras la versión variable devuelve un iterador a un objeto variable.

Conclusión

La palabra clave const no es fácil de entender, porque se usa en muchos conceptos. Sin embargo, es la pieza que permite a C++ de ser uno de los pocos lenguajes a conseguir la “const-correctness“. Esta a su vez significa que ya el compilador puede comprobar que no se asigna un valor a algo que sea de sólo lectura. Aunque muchos programadores hacen caso omiso al const, conviene tener una noción de ellos cuando uno trabaja con la STL, ya que muchos errores de compilación (que suelen tener un mensaje largo) se deben a confundir iterator con const_iterator.

Aquellos que buscan más motivación puedo recomendar el artículo “Por qué usar const en C++“. Pero también hay consuelo para quienes buscan la manera de esquivar las constantes: puedo eliminar cualquier const con const_cast.

Lectura adicional

Escribe tu dirección de correo electrónico para suscribirte a este blog, y recibir notificaciones de nuevos mensajes por correo.

Únete a otros 51 seguidores

Archivos

junio 2018
L M X J V S D
« Abr    
 123
45678910
11121314151617
18192021222324
252627282930  
Anuncios