You are currently browsing the monthly archive for mayo 2010.

¿Qué es un control de versiones?

Un control de versiones una herramienta para el desarrollo de software que facilita principalmente dos funciones:

  1. Ayuda a que varios programadores pueden trabajar en paralelo.
  2. Permite mantener varias versiones de un mismo programa a la vez. (De ahí “control de versiones”)

Trabajar en paralelo

Si dos programadores realizan cambios en un mismo fichero fuente, entonces sobreescriben los cambios del otro cada vez cuando lo guardan. Incluso si un equipo de trabajo llega a organizarse para que esto no sucede, todavía es probable que todos los “prints”, “ifs”, y demás modificaciones temporales que un programador pueda poner para probar su código molesten a los demás. Si el programa no compila entonces todos los demás están parados hasta que se haya arreglado el error.

Aquí ayuda un control de versiones. Los ficheros “buenos” – es decir una versión que al menos compila – están en un servidor, el “repositorio”. Un programador trabaja sobre una “vista” a estos ficheros (habitualmente una copia local en el ordenador del programador). Si quiere cambiar un fichero, entonces debe “sacarlo del control” (hacer un check-out o desproteger). Mientras tanto los demás programdores siguen viendo la versión original del fichero y no están afectados por las modificaciones de un miembro de equipo.

Cuando el primer programador cree que su versión modificada funciona razonablemente bien, entonces la “sube” (mete, hace un check-in o la protege). Con esto se ha creado una nueva versión, que será la que verá todo el mundo que “actualice su vista”. Un control de versiones no reemplaza a una copia de seguridad, porque no se guardan los cambios en el servidor hasta que el programador lo ordene. Y esto puede durar un mes en ocasiones.

Merge

Un control de versiones sólo permite “subir” un fichero, si es el sucesor de la versión de que fue sacado. Puede suceder que dos programadores sacan sus versiones personales de la misma versión. El primero puede hacer un check-in de sus cambios sin problemas, pero el segundo debe conciliar sus cambios con las modificaciones del otro programador. Un fichero sacado no se actualiza con las versiones que los otros programadores hayan metido, ya que sobreescribirían las modificaciones propias.

La solución es un “merge” (inglés para combinar). Habitualmente se abre una pantalla con las tres versiones afectados: La versión original de donde se partió, la versión a meter y la última versión bajo control. En un caso simple se ha modificado el fichero en puntos diferentes y se pueden simplemente incluir los cambios de la versión más reciente en la versión a meter. Dos cambios en una misma línea de código causan un “conflicto” que el programador debe resolver a su criterio y con la ayuda del otro programador. Muchas veces un control de versiones puede combinar versiones sin conflictos de forma automática. No es posible hacer un merge en ficheros binarios.

Mantener varias versiones

En muchos programas de uso común es habitual mantener varias versiones. Por ejemplo, no todo el mundo está dispuesto a instalar un nuevo sistema operativo cuando Microsoft publique una nueva versión de Windows. Así Microsoft debe mantener varias versiones de Microsoft Windows al mismo tiempo que está desarrollando la próxima generación.

¿Qué significa varias versiones? Cada vez cuando un programador sube sus cambios, se crea una nueva versión que el sistema enumera con un número secuencial. Podemos decidir que alguna de estas versiones, digamos la versión 3124, será destinada a ser una “release” oficial. Entonces se “etiqueta”, por ejemplo como “Windows 8 alpha” y se crea una nueva “rama” (branch en inglés). La rama principal (“trunk” en inglés) es la en que los programadores hagan el desarrollo nuevo. La rama “Windows 8 alpha” existe en paralelo y sirve para corregir los errores que todavía pueda tener. En esta versión interesa más llegar a una versión “estable”, un “release”, que añadir un nuevo desarrollo.

Lógicamente, todos los errores que se encuentran en la versión “Windows 8 alpha” habrá que corregir también en la rama de desarrollo principal. También puede suceder que alguna funcionalidad nueva de la rama de desarrollo entra en la rama de la versión “release”. En general, los programadores tienen que modificar varias versiones y el control de versiones les permite determinar cuales.

Como es imaginable, una rama de una release puede bifurcarse también. Una sigue con las correcciones de la versión (oficial) 8.0, la otra está destinada a incluir mejoras para la release 8.1. En el momento que una versión ya no incluirá nuevas funcionalidades se “congela”. Cuando ya no se trabaja sobre ella ya no está “soportada”.

Pequeña lista comentada de control de versiones

Hasta algunos años, la comunidad de código abierto ha usado con mucho gusto el CVS (Concurrent Versions System). Ahora se usa más el Subversion, que ofrece un cliente integrado en el explorador de Windows que se llama TurtoiseSVN. Es todo gratis y una buena solución para la mayoría de los proyectos.

Cuando gratis no es un requisito puede convenir usar IBM® Rational® ClearCase®, un programa completo que permite un uso por línea de comando y por interfaz gráfico. Microsoft Visual SourceSafe es un extra con las versiones de empresa de Visual Studio, pero que a veces da impresión de ser ni visual ni seguro. La ventaja que tiene es que se integra bien con el Visual Studio.

Quien piensa en grade debe tener en cuenta que eligir una herramienta de control de versiones es una decisión estratégica de la empresa. Una base de datos es lo más difícil de actualizar en un programa y un control de versiones no es otra cosa que una base de datos donde los datos son ficheros. Es bastante probable que una empresa todavía usa el control de versiones de hoy en diez años.

Categorías de conenedores de la STL
Elementos simples Elementos “aparejados”
std::vector std::map
std::deque std::multimap
std::list
std::set

Los contenedores de la Standard Template Library (STL) de C++ pueden dividirse en dos categorías. La mayoría de los contenedores acoge a elementos simples. Estos contenedores son vector, deque, list y set. Al contrario de los contenedores simples están los contenedores asociativos map y multimap que guaradan a una pareja (pair) de valores: una clave y un valor. Se llaman asociativos porque asocian claves con valores.

Internamente un mapa está implementado como un árbol binario que acoge un elemento simple de tipo pair<T_KEY, T_VALUE>. Desgraciadamente los árboles no son un elemento público de la STL aunque prácticamente todas las versiones los implementan.

Insertar

Un problema práctico con los contenedores asociativos es que insertar un nuevo elemento acaba en líneas muy largas. En lugar de escribir algo tan complicado como

std::map.insert(std::pair<T_KEY, T_VALUE>(my_key,  my_value));

preferiríamos un método

std::map.insert(my_key, my_value);

Desgraciadamente este método no forma parte del estándar. Una solución a este problema es crear una clase propia que hereda del mapa estándar. Esto trae la ventaja que podamos añadir tantas funciones que nos convienen y la desventaja que hay que escribir esta clase. Para hacerla completa debemos al menos ofrecer todos los constructores y métodos de operator=.

Nuestro nuevo método de insertar sería entonces algo así:

template <class T_KEY, class T_VALUE>
class Map : public std::map<T_KEY, T_VALUE>
{
    std::pair<iterator,bool> insert(const T_KEY& key, const T_VALUE& value)
    {
        return insert(std::pair<T_KEY, T_VALUE>(key, value);
    }
};

Operator[] para mapas constantes

Otra función práctica que muchas veces falta es el operator[](const T_KEY& key) para mapas constantes. La versión no constante crea un nuevo elemento para la clave si no existe. En un mapa declarado const no podemos hacer esto, pero podemos arriesgar una excepción. Está en manos del usuario del método de asegurar que sólo llama a operator[] const para claves existentes o capturar las excepciones. Aquí está el código:

template <class T_KEY, class T_VALUE>
class Map : public std::map<T_KEY, T_VALUE>
{
    template <class TKey, class TValue>
    const TValue& operator[](const TKey& key) const
    {
        // Esta línea causa una excepción cuando find devuelve end()
        return this->find(key)->second;
    }
};

Iteradores sólo sobre valores

Otra utilidad que falta son iteradores sólo sobre los valores del mapa, es decir un iterador cuyo operator* no devuelve un std::pair<T_KEY, T_VALUE> sino un T_VALUE. Uno puede conseguir esto escribiendo una clase value_iterator, que contiene el iterador de mapa habitual como elemento. Desafortunadamente esto conlleva mucho trabajo ya que será necesario implementar todas las funciones del iterador convencional – muy especialmente todos los operadores sobrecargados.

Conclusión

En fin, hemos propuesto algunas ideas de como se pueden ampliar contenedores estándar por métodos propios – en este caso el contenedor map. Conviene tener en mente que crear una clase propia que hereda de clases estándar es una manera más limpia de codificar que crear funciones globales, macros o simplemente copiar y pegar las expresiones complejas y feas que la interfaz del contenedor estándar nos obliga a tener.

Referencias

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

¿Qué es la diferencia entre un científico y un ingeniero?

Un científico sabe, porque no funciona. Un ingeniero no sabe, porque funciona.

¿Cómo se sabe si una ecuación forma parte de la matemática, la física o la ingeniería?

Matemática es hallar una ecuación sin más razón. Si se encuentra alguna
utilidad para ella, se llama física. Si encima la utilidad es algo que gente quisiera usar, se llama ingenería.

El matemático demuestra que una ecuación es válida para todos los elementos pensables. El físico se limita a calcular que la aproximación de la ecuación dé un error menor que la tolerancia de medida para todos los elementos reales en el vacío. El ingeniero se limita medir que un objeto útil no requiere una ecuación más complicada que a = b·c. El informático asume además que b y c sean valores enteros menores de 32000.

Hay una experiencia en la programación que dice

Primero hazlo funcionar, entonces hazlo rápido.

Lo que quiere decir es que no te preocupes (demasiado) sobre la velocidad con que tu programa se ejecutará a la hora de crear.

Esto viene de la experiencia que los problemas de velocidad muchas veces surgen de donde no se pueden predecir. Poco sirve optimizar un código para que corra un 30% más rápido cuando la mayor parte del tiempo no hace nada más que esperar a una conexión de red lenta. Comprar un ordenador más potente es muchas veces más barato que pagar un programador para optimizar el código. (De hecho un ordenador nuevo cuesta menos que un salario mensual.) Tampoco afecta mucho al programa entero cambiar esa sentencia SQL lenta por una más optima. Salvo en entornos concretos como el tratamiento de datos en tiempo real, la velocidad del código no es tan importante como la velocidad del programador para terminar el programa.

No obstante hay unas prácticas de programación que suben la velocidad de ejecución sin perder mucho tiempo a la hora de usarlas. Aquí presentamos algunas de ellas.

Evita llamar a las mismas funciones con los mismos parámetros. Una llamada a la misma función con los mismos parámetros suele devolver el mismo resultado. Es más rápido guardar este resultado en una variable (o constante) y usar su valor, porque en general no podemos saber si la función ejecuta un cálculo costoso en cada llamada. En el caso más rápido la función no hace otra cosa que devolver un simple valor, pero nunca va a ser más rápida que la variable. Esta regla es especialmente importante cuando su valor de retorno es usado en una condición de fin de bucle.

Es decir, en lugar de una construcción así

for (int i = 0; i < tamaño_de_mi_vector(); ++i)
{
    // Haz algo
}

usar esta práctica

const int tamaño = tamaño_de_mi_vector();
for (int i = 0; i < tamaño; ++i)
{
    // Haz algo
}

Evitar funciones con muchos parámetros. Cada parámetro incluye una copia de algo. Separa el código de una forma que minimiza el número de parámetros. Es cierto que lo óptimo por este punto de vista sería no separar el código en absoluto, pero tampoco queremos perder de la vista la velocidad del programador de entender el programa.

No devolver arrays y objetos grandes. Lo que es un objeto grande depende del lenguaje. C++ devuelve una copia de un objeto (costoso), Java y PHP devuelven nada más una referencia (rápida). Sin embargo, PHP copia los arrays. Es decir devolver un objeto que contiene un array es más rápido en PHP que devolver directamente el array. C++ permite devolver referencias a objetos también, pero el programador debe asegurar que el objeto existe después de la llamada a la función. Una manera de conseguirlo es que el valor de “retorno” es una referencia a un objeto que suministra el usuario de la función como parámetro.

// Esta devolución llama a un constructor de copia.
UnaClase una_funcion()
{
    UnaClase un_objeto;
    return un_objeto;
}

// Esta función pide como parámetro el objeto donde escribir el resultado.
// Nota el ampersand & que indica la referencia en lugar de copia.
// La función no necesita devolver ningún valor.
void otra_funcion(UnaClase& referencia_a_resultado)
{
    referencia_a_resultado = // lo que tú quieras
}

// Estas funciones se llamarían así.
// Nota que la técnica de pasar una referencia no permite declarar
// la variable constante.
const UnaClase un_valor = una_funcion();
UnaClase otro_valor;
otra_funcion(otro_valor);

Usar el incremento/decremento pre-fix en lugar de post-fix para operadores sobrecargados (en C++). Esto suele “ahorrar” dos llamadas al constructor de copia. Este tema está tratado en un artículo específico.

El lenguaje C++, como muchos otros lenguajes, tiene un operador de incremento pre-fix y post-fix. El operador pre-fix ++p tiene el mismo efecto sobre una variable entera int p como un incremento post-fix p++: el valor se incrementa en uno. La diferencia está que la expresión ++p evalúa al valor de p después del incremento (pre-fix como pre como primero), mientras la expresión p++ tiene el valor de p antes del incremento (post como después). Sin embargo, en muchas ocasiones no se hace uso de esta diferencia y simplemente se quiere añadir uno a la variable p.

No importa mucho qué operador se usa para tipos básicos, porque el compilador añade el cálculo de “más uno” en el código ejecutable donde hace falta. Sin embargo hay una diferencia importante cuando se usan operadores sobrecargados en clases. Lo mismo que vamos a demostrar aquí para un operador de incremento ++ vale también para un operador de decremento --.

Una típica implementación de un incremento sobrecargado sería algo así:

class MiClase
{
private:
    int mi_valor;
public:
    // El operador pre-fix (argumento void)
    MiClase& operator++(void)
    {
        // Increméntame internamente (también puedo usar ++mi_valor)
        mi_valor += 1;

        // Devuelve una referencia a mí mismo
        return *this;
    }

    // El operador post-fix (argumento int sin nombre)
    MiClase operator++(int)
    {
        // Guarda una copia en una instancia temporal
        MiClase temp(*this);

        // Increméntame internamente (también puedo usar ++mi_valor)
        mi_valor += 1;

        // Devuelve la copia temporal con el estado anterior
        return temp;
    }
};

A primera vista vemos que el pre-incremento es mucho más simple. Incrementa el estado de la clase y devuelve una referencia a la instancia.

El post-incremento es más complicado. Tengo que crear una copia para no perder el estado anterior que finalmente devuelvo también como copia y no como referencia. Es decir, tengo que llamar dos veces al constructor de copia cuando no necesito hacerlo para el operador pre-fix.

Por este motivo es preferible usar el operador pre-fix para incrementar y decrementar objetos. Aunque esta regla no sea necesario para variables de tipos básicos es preferible hacerlo para tener un estilo único.

Doxygen es una herramienta de documentación automatizada muy potente. Como muchos programas viene con la configuración para el inglés americano por defecto. Por eso puede haber problemas a la hora de procesar bien comentarios que usan letras con acentos como suele haber en español y muchos otros idiomas.

Una solución al problema es guardar los ficheros con la codificación UTF-8, que es la que viene por defecto también en phpDocumentor. Sin embargo, doxygen permite especificar explícitamente qué codificación a usar.

Si usas el interfaz gráfico Docwizard, entonces puedes pinchar en la pestaña “Expert”, y luego en el menú de los “Topics” seleccionas “Input”. A la derecha hay un campo que se llama INPUT_ENCODING. Cambia el valor (“UTF-8” por defecto) a la codificación de tus ficheros fuente. Para el español suele ser la codificación ISO-8859-1. Obviamente sólo puedes usar una codificación de caracteres para un proyecto.

Si no usas el interfaz gráfico puedes buscar por la cadena de texto INPUT_ENCODING en el fichero de configuración.

Si no estás seguro qué codificación a elegir, entonces puedes visualizar la documentación generada y jugar con la codificación de los caracteres del navegador hasta tu documentación  salga bien. La codificación puedes cambiar (en Mozilla Firefox) con el menú View (Ver) -> Character Encoding. Cuando haces estas pruebas fíjate en los textos que has escrito tú en la documentación; y no en los títulos que añade doxygen. Cuando pones la codificación correcta en el campo INPUT_ENCODING, tanto los textos generados a partir de los comentarios como los que añade doxygen saldrán con los acentos bien. Recuerda volver poner tu navegador a autodetectar la codificación.

Por cierto, también se puede modificar la codificación del fichero de configuración de doxygen. En la pestaña “Expert” seleccionas en el menú “Topics” el punto “Project”. A la derecha hay un campo que se llama DOXYFILE_ENCODING. Cambia el valor (“UTF-8” por defecto) a la codificación desdeada.

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

mayo 2010
L M X J V S D
« Abr   Jun »
 12
3456789
10111213141516
17181920212223
24252627282930
31