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

El lenguaje C (y C++) ofrece tres maneras de definir constantes. En este artículo queremos aclarar las diferencias entre #define, enum y const. Como veremos a continuación, casi siempre es preferible usar const.

#define CONSTANTE_DEFINE 5
enum T_ENUM { CONSTANTE_ENUM = 5 };
const int CONSTANTE_VARIABLE = 5;

Los tres valores CONSTANTE_DEFINE, CONSTANTE_ENUM y CONSTANTE_VARIABLE valen lo mismo (tienen el valor 5) pero tienen diferencias significativas a la hora de compilación.

La directiva #define es una palabra clave del pre-procesador. Técnicamente se podría decir que no forma parte del lenguaje C. Antes que el compilador ve el código, el pre-procesador ya a reemplazado la macro CONSTANTE_DEFINE por su definición – en nuestro caso el símbolo 5. Es decir, en todos los sitios donde aparece el identificador CONSTANTE_DEFINE, el compilador ve un 5 como si nosotros mismos hubiéramos escrito un 5 en estos sitios. Es posible que el compilador ni siquiera puede decirnos qué exactamente está mal en una compilación fracasa, porque no sabe nada del identificador CONSTANTE_DEFINE.

Como la directiva #define sólo tiene un significado textual y carece de cualquier información de tipo, es aconsejable evitar estas directivas y const siempre y cuando sea posible. Sin embargo, el hecho que #define no tiene un tipo asociado puede ser también una ventaja a la hora de hacer conversiones de tipos implícitas. No obstante, todavía es cuestionable si conversiones implícitas son deseables.

Los programadores saben que físicamente se codifica una constante definida con enum como un const intconceptualmente una constante definida con enum no tiene valor numérico asociado. El concepto es todo al contrario: usar identificadores más significativos como LUNES, MARTES, AZUL, VERDADERO en lugar de números en que uno tiene que saber que un “1” significa lunes, otro “1” azul y un tercero verdadero.

Por lo dicho conviene recordar que no hay nada mejor para representar el concepto de uno que el símbolo “1”. Si queremos añadir un número específico entonces mejor ponemos este número en lugar de inventarnos una constante cuyo valor luego debemos buscar por ahí.

La razón por qué se pueden asignar valores específicos a constante de enumeración es para poder adaptarlas a valores exteriores. Por ejemplo, podría definir un tipo que encapsula constantes de error de HTML.

enum T_HTML_ERROR
{
    E_PAGE_NOT_FOUND = 404;
}

Por asignar de forma inteligente los valores a las constantes puedo asignar fácilmente un valor de tipo int a una variable de tipo T_HTML_ERROR mediante un cast. Por ponerle el nombre E_PAGE_NOT_FOUND al valor 404, será más fácil entender el código.  Sin embargo, todavía debo hacer un cast y por eso, en general, es un mejor estilo definir constantes con const int que luego se asignan a una variable de tipo int. Como ya hemos dicho, la idea de una constante enum es justamente no tener en cuenta su valor.

Una constante enum como LUNES no es completa en sí, sino forma parte del conjunto de constantes que forman un tipo de enumeración como T_DIA. Desde el punto de vista del lenguaje no tiene sentido asignar una constante a otra cosa que a una variable de este tipo de enumeración. En lenguajes más puristas como el Pascal es incluso prohibido convertir enumerativos a su representación numérica interna.

Finalmente queda la opción de definir constantes mediante const. Este método permite definir el tipo y el valor de la constante. Es incluso posible esconder el valor de la constante, definiéndola extern en el fichero de cabecera y asignar su valor en el fichero fuente. Aunque codificar constantes con const es la manera más limpia, suele ser la menos usada. La razón será más bien costumbre porque técnicamente hay poca razón de usar #define o enum.

El compilador puede comprobar el tipo de las constantes con const que las hace mejor que los #define. Al mismo tiempo permite las misma conversiones implícitas. Si no tengo claro si quiero codificar un número como entero o una cadena de texto, puedo usar un typedef para poder modificar también el tipo de la constante con facilidad.

Los enum sólo pueden representar valores enteros, mientras una constante puede tener cualquier tipo: un número flotante, una cadena de texto, una estructura compleja. Todo. Una constante const tiene una dirección de memoria, por lo cual se puede pasar un puntero a ella. El valor de una constante const puede ser público en el fichero de cabecera o escondido en el fichero fuente.

En fin, hemos visto las diferencias y concluimos que por regla general es preferible usar constantes declaradas por const. Las excepciones son:

  • Podemos usar #define cuando efectivamente necesitamos hacer algo antes de compilar. Por ejemplo, constantes de error que dependen de la plataforma, en general dentro de una compilación condicional con #if#endif.
  • Podemos usar enum si sólo queremos nombres sin necesidad de saber los valores con qué se representan internamente.

Por todo lo demás: const

Referencias

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.

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 55 seguidores

Archivos

agosto 2019
L M X J V S D
« Nov    
 1234
567891011
12131415161718
19202122232425
262728293031