You are currently browsing the category archive for the ‘Trucos’ category.

Si quieren ver los ficheros en tu ordenador en un control policial u otro organismo de seguridad o inseguridad, git te puede ayudar a esconder datos. Crea un repositorio git y crea dos ramas. En la rama master guardas datos no sospechosos y en la otra con nombre no sospechoso guardas los datos que quieres proteger. Entonces, con git checkout master puedes rápidamente esconder lo que no quieras que vean. La carpeta “.git” está escondida y no contiene ficheros con nombres claros.

No olvides que esta forma de esconder datos sólo funciona si el atacante no piensa en este truco y no conoce git. Esta propuesta camufla el hecho que hay datos de interés, pero no los encripta. También ten en cuenta que un cambio de la rama puede durar un tiempo si son muchos ficheros.

Lectura adicional

Anuncios

Hay muchos artículos que tratan de como formar una contraseña “buena”. El problema es cómo memorizar 50 diferentes. En este artículo presento una manera que se basa en memorizar una regla para formar contraseñas en lugar de las contraseñas mismas.

El problema

Como te preocupa la seguridad de tus datos, usas varias cuentas de email distintas. Los correos más importantes te dejas enviar al servidor de la NSA, porque sabes que ahí no se borra nunca nada. Como tienes pocos correos importantes, nunca te acuerdas de la contraseña para abrir la página de correo. Lo que, sí, te acuerdas es que cambias tu contraseña a menudo por seguridad. Con este método te aseguras que aún no sabes tus credenciales si te acuerdas de alguna contraseña porque ya no estás seguro si es la actual.

¡Así de frustrante es hacer contactos seguros por Internet!

Una forma de resolver este problema es mantener una base de datos de contraseñas como KeePass que a su vez está protegido por una contraseña maestra. Así sólo necesitas saber una contraseña para saberlas todas (y la NSA y demás interesados también). Pero conlleva el inconveniente que necesitas esta base de datos siempre a mano y esto a veces no es tan fácil. Instalar un programa así en tu smartphone y, además, confiar que no envie tu base de datos por ahí ya es un reto. El otro es abrir la base de datos, leer la contraseña correspondiente y editarlo en la página que quieres abrir. No es fácil si es la contraseña que da acceso a tu teléfono.

La vida sería mucho más fácil si tuvieras todas las contraseñas en la cabeza. La buena noticia es: se puede. En lugar de memorizar contraseñas memorizas una regla de como formar una contraseña. Los “datos de entrada” a que aplicar esta regla los obtienes directamente del sitio en que quieres entrar.

Los ingredientes

Queremos una contraseña diferente para cada sitio. Así el sitio debe ser un ingrediente en la contraseña de alguna forma. También quieres cambiar tu contraseña regularmente. Así el tiempo también debe ser una parte. Personalmente considero los siguientes ingredientes en cada contraseña.

  1. Una parte “fija”. Esta parte sirve para hacer la contraseña más larga, ensamblar los demás datos en una frase completa y subir la entropía (o calidad de encriptación). No obstante, la parte fija puede omitirse si uno quiere.
  2. El sitio. El sitio es adonde quiero entrar: “el teléfono”, “NSA”, “mi ordenador”, “facebook”.
  3. El tiempo. Conviene usar el año para la noción de tiempo, pero esto depende de qué a menudo cambias tu contraseña en general.
  4. La versión. La versión tiene un significado variado según tus necesidades. Si cambias tu contraseña cada més, podría ser el mes. Si cambias tu contraseña cuando en el periodico sale que fue robada, entonces la versión te permite crear una nueva contraseña y todavía usar el mismo año para “el tiempo”. No obstante, ninguna versión también es una versión válida. Se puede omitir.

Con todos esto campos se podría crear una contraseña como “Mi primera contraseña para la NSA en 2015”. Tiene una parte fija (“Mi … contraseña para … en”), un sitio “la NSA”, el tiempo (“2015”) y una versión (“primera”).

De hecho, esto ya es toda la mágica. Con esta regla puedes crear una contraseña distinta y con fecha de caducidad para cualquier sitio en que entres. En la contraseña actual entra el año actual. Si no funciona, debe funcionar la contraseña del año pasado. (Y si ésta tampoco considera cerrar la cuenta.)

Encriptar tu contraseña

Esta contraseña ejemplo arriba es fácil de crear y fácil de adivinar. Mejor es una contraseña que sólo es fácil para ti. Además, si uno ha descubierto una de tus contraseñas, entonces no quieres que lo tenga fácil adivinar las demás. Así debes poner el sitio, el tiempo y la versión en una manera encriptada.

La versión

La versión es la más fácil. Lo que necesitas es alguna enumeración con un orden definido. Los números natureles 1, 2, 3 son un ejemplo. Más ejemplos pueden ser las ciudades en que has vivido, los nombres de los peces en tu aquario en el orden de entrada, tus profesores de matemática en la escuela o los años en que has ganado el campionato de bailar Merengue. Todos estos ejemplos tienen en común que sólo tienen un orden (cronológico) en tu vida y que no aparentan ser demasiado ordenados por alguien más. Normalmente no necesitas muchas versiones diferentes. Así te vale con cuatro cosas.

El tiempo

Saber que “2015” representa un año es bastante obvio. Así es mejor camuflarlo. No camufla mucho eligir otro calendario como años budistas o judios o representar el año por el animal de turno del calendario chino. Para ti puede ser exótico pero para un posible atacante puede ser lo más normal del mundo.

Lo que funciona mejor que hacer un cálculo mátematico que no sea una simple adición o subtracción ya que estos dejan la estructura del número todavía demasiado obvia para alguien que ha robado varias de tus contraseñas. Mejor es una multiplicación con un número pequeño que sólo sabes tú como los años que tiene tu perro o el número de cuadros en la pared. Ayuda si este número se cambia cada año, porque así no multiplicas siempre con el mismo número que también dejaría una estructura visible como los múltiple de 5 de todos los años.

Mejor aún para romper la estructura de número de años consecutivos es aplicar un algoritmo no líneal. Por ejemplo, los años pares multiplicas por el número de cuadros y los impares por los años de tu perro. Ideal es la combinación de varios cálculos. Pero tu procesador de cálculo cerebral probablemente prefiere que te limites a algo que todavía seas capaz de hacer bajo la influencia de drogas socializadas.

Como los dos primeros cifras del año no cambian mucho, es mejor hacer los cálculos sobre el último o los dos últimos dígitos del año.

El sitio

¿Es obvio que una contraseña que contenga “para la NSA” será para la NSA? Pues, ¡sí! Tienes dos formas de tratar este problema.

La versión número uno trata de camuflar el sitio de una forma similar que el tiempo. Aplicamos un cálculo sobre letras. Por ejemplo, escogemos para cada letra la próxima del alfabeto. La “NSA” sería entonces “OTB”. Puedes intentarlo con más fantasía: si es un vocal, entonces avances tantas letras como los años que tiene tu perro y si es un consonante retrocedes tantas letras como suman los dígitos de este año. (Por ejemplo, 2 + 0 + 1 + 5 = 8 posiciones) De esta manera cambias incluso la encriptación cada año.

El problema con la versión número uno es que no es práctico. Si has calculado que la “NSA” sea algo como “OTB”, entonces empiezas memorizar la “NSA” como “OTB”. El cálculo con letras no es tan fácil de manejar por el cerebro que el cálculo con números. A menos que seas una excepción, te pararás al teclear el sitio porque el cálculo de letras te cuesta tanto esfuerzo mental.

Así llegamos al número dos: ¡no hacer nada! Aparentemente. Alguien que ha descifrado tu contraseña siempre sabe para que sitio sirve, porque encontrar una llave sin saber la cerradura a que corresponde no es un peligro. Así no revelas demiasado que la contraseña para la “NSA” contenga las letras “NSA”.

Lógicamente no quieres que el atacante sólo necesita reemplazar “NSA” por “Casa Blanca” para obtener el pase a algún edificio emblemático. Pero esto puedes resolver de una forma más sútil. Y para esto conviene la parte “fija”.

La parte “fija”

He puesto “fija” entre comillas porque no necesita ser tan fija” La puedes usar para subir el número de combinaciones posibles.

Digamos tu parte fija es “campeón”. Si el sitio tiene tres letras, entonces reemplazas las primeras tres letras por puntos “…peón” o escribes la tercera letra en mayúscula “caMpeón”. Cómo no quieres que un atacante simplemente reemplaza “NSA” por otra institución de tres letras, entonces lo pones un poco más complicado. Reemplazas la tercera letra por la primera del sitio y añades la última. Así obtienes “caNpeónA”.

En general puedes seguir las recomendaciones para formar una contraseña para crear la parte “fija”. Puedes mezclarla con la información de las otras parte tiempo, sitio y versión. Así la parte “fija” se modifica y rompe la estructura aparente de la contraseña. Aunque un atacante sepa en qué parte pones el sitio y el tiempo, no lo tendrá tan fácil en saber como estas partes modifican la parte “fija”. De esta forma te permites no necesitar camuflar las otras partes mucho.

A probar

La primera cosa es inventarse una regla. Para comprobarla escribe varias contraseñas formadas por ella y mira cuanto se distinguen. Cuanto más mejor – especialmente aquellos que vienen de datos similares porque, por ejemplo, sólo cambiaste el año o el sitio.

La segunda cosa es aplicar la regla y esto ya cuesta bastante más coraje. Ahora te toca cambiar contraseñas y confiar que no las olvides. Para el principio te recomiendo que sólo cambias unas pocas que usas a menudo y, por seguridad, te las apuntas en algún fichero encriptado offline. Tras un par de semanas estás acostumbrado a la nueva forma.

Además, comienza con una regla que no te cuesta demasiado pensarlo. Tras medio año estás más entrenado y cambias a una más compleja. De hecho, cambiar la regla a veces puede mejorar la seguridad como lo hace cambiar la contraseña. Pero como tu regla ya contiene un campo de tiempo, no hace falta cambiarla tan a menudo.

Un problema te puede surgir por la ortografía. ¿Nombres propios comienzan mayúsculas o los escribo como los hace los propietarios? Es decir, “Facebook” o “facebook”? Mi recomendación es póntelo fácil: todos los nombres escribes igual. Todos minúsculas o primera mayúsculas o la última letra mayúscula. Así evitas el problema.

Un problema más difícil es la denomicación de los sitios. ¿Tu cuenta de correo lo tienes en “Google” o “Gmail”? ¿En qué piensas cuando tecleas la contraseña de acceso a tu ordenador? ¿El nombre que tiene en la red o simplemente “mi ordenador”? Verás que muchas veces no es tan fácil determinar un sólo nombre obvio. Para esto no hay una regla de oro. Intenta sentir en lo que piensas primero cuando ves tu ordenador, tu teléfono, tu blog o tu cuenta de correo y esto escoges como nombre de sitio. Esto tendrá éxito porque muchas veces pensamos primero en lo mismo cuando vemos la misma cosa. (Lógicamente esto no es recomendado para quien piensa siempre en lo mismo no importa lo que vea.)

También para la denominación ponlo fácil. “Quizá no es creativo llamar “ordenador” a tu ordenador pero lo obvio para uno no tiene por qué serlo para otro. (Si no lo crees sigue una conversación entre tu padre y tu madre.)

Tampoco lánzate demasiado. Empieza cambiar la contraseña en los sitios que ya tienes claro como denominarlo. Quizá sería perfecto cambiar todas las contraseña a tu nueva regla, pero si sólo cambias un 20 por cien, ya has ganado algo. Debes aprender una nueva forma de hacer las cosas y esto cuesta tiempo. También verás como querrás mejorar tu regla inicial tras cierto tiempo. Así empieza con poco. No es un error mantener una base de datos encriptada con contraseñas mientras tanto – sobre todo con las que usas poco.

Trampas

Aunque la regla sea perfecta, el mundo no lo es. El dogmático intenta cambiar el mundo, el pragmático ajusta el dominio en que la regla es válida. Como no falta opiniones sobre como hacer el primero, aquí tratamos la segunda manera.

Contraseñas de longitud limitada

¡Sí! Esto existe hasta hoy en día: que una contraseña no deba contener más que ocho caracteres o al menos no más que 16. Es un aviso que el sistema guarda la contraseña directamente en lugar de algún hash como MD5, ya que este hash siempre tiene la misma longitud.

Si tu contraseña no cabe, entonces busca alguna forma de abreviarla. La más obvia es abreviar cada palabra con la primera letra. O sólo las palabras y no las cifras. O sólo escribes la letra que escribes en mayúsculas. En fin, te hace falta encontrar una regla de cómo abreviar tu regla. O directamente inventas una regla con contraseñas cortas aunque esto tampoco es la mejor solución ya que baja la seguridad en todos los sitios.

Claves de cifras

Claves de cifras hay muchas: la cerradura de números de tu maleta, el código para la caja fuerte en el hotel y el PIN de tu teléfono. Algunos sistemas ni siquiera te permiten cambiar el código como muchas tarjetas de crédito. Ahí olvídate de cualquier regla de formar contraseñas. Mejor aprendas una para memorizar números.

Es lógico que tu reglar de arriba no va a funcionar con códigos numéricos o ¿cómo escribes el sitio “caja fuerte” con los cuatro cifras que te permite sistema?

La solución es inventarse otra regla que genera sólo dígitos. Hazte libre de cualquier regla que acabas de inventar para tu ordenador y piensa en algo puramente numérico. Fechas son malas, pero sólo si se relacionan públicamente contigo como tu cumpleaños. Algún otro día que no quieres olvidar, aunque no quieres decir a otros qué día era y qué hiciste entonces, es una mejor opción.

Una regla numérica es una secuencia matemática. Por ejemplo, 123456. Mejor ejemplo, 112358, que es la secuencia Fibonacci. Aún mejor ejemplo, 126258, que la secuencia de Fibonacci multiplicado por la posición del dígito.

Al contrario de contraseñas, no es necesariamente buena idea incluir el tiempo en una clave numérica, porque muchas claves no se cambian casi nunca. Piensa en la maleta con cerradura numérica y sólo usas cada dos o tres años. ¿En qué año pusiste el código por última vez? ¿No lo prestaste a alguien que seguramente lo cambió? Al final quizá ni es la peor idea de siempre poner la fecha de contraer matrimonio. Si alguien la adivina, pues, entonces al menos a uno puedes preguntársela.

Nota del autor


A la hora de escribir este artículo no conozco ningún otro que trata este tema. Así es posiblemente la primera publicación de esta idea. Si quieres citarlo en una obra científica puedes poner el enlace a esta página (a falta de algo mejor) o te pones en contacto conmigo (con el enlace “Envía un mensaje al autor” al lado).

Referencias

  • KeePass – “caja fuerte” para contraseñas

Lectura adicional

Con “guardianes de alcance” me refiero a objetos locales en una función que tienen algún efecto sólo por existir. Sólo tienen un constructor y un destructor, que suelen reservar y liberar algún recurso.

El mutex locker

Una tarea típica en la programación multi-hilo es usar un mutex. El hilo que reserva el mutex puede seguir; los demás hilos que quieren reservarlo deben esperar. En el código, esto suele aparacer así:

// Definición del mutex
Mutex mi_mutex;

// Una función que usa el mutex
void mi_funcion
{
    // Reserva el mutex o espera hasta que pueda reservarlo
    mi_mutex.lock();

    // Haz algo

    // Libera el mutex
    mi_mutex.unlock();
}

Este emparejamiento de lock y unlock aparece frecuentemente en un programa multi-hilo. No obstante, el unlock puede ser necesario en varios puntos, cuando una función tiene varias salidas – por ejemplo por un return en medio o por un lanzamiento de una excepción. Poner un unlock en cada rama engorda la función y es fácil olvidarse de algúno. Pero olvidarse de liberar algún recursos puede dar a errores difíciles de localizar.

Por suerte existe una manera de no olvidarse de un unlock. Esto se puede conseguir mediante una clase MutexLocker. El constructor de esta clase reserva el mutex y el destructor lo libera.

class MutexLocker
{
    // El constructor reserva el mutex
    MutexLocker(Mutex& mutex_a_reservar)
    : _mutex_reservado(mutex_a_reservar)
    {
        _mutex_reservado.lock();
    }
    
    // El destructor libera el mutex
    ~MutexLocker(void)
    {
        _mutex_reservado.unlock();
    }
    
    // La REFERNCIA al mutex que se quiere reservar
    Mutex& _mutex_reservado;
};

El código arriba se modifica así

// Definición del mutex - esto no se modifica
Mutex mi_mutex;

// El uso, sí, se modifica
void mi_funcion
{
    const MutexLocker mi_mutex_reservado(mi_mutex)
    // Haz algo
}

Como vemos, sólo hace falta una línea en la función. Cuando la variable mi_mutex_reservado sale fuera del alcance, se llama al destructor que libera el mutex. Esto es así, no importa por qué salida la función termina. De esta forma tenemos un “guardián de alcance”. Nos limpia todo al salir del alcance de la función.

En general, se puede declarar const a un guardián, ya que no lo modificamos durante su vida. Ni siquiera podríamos, ya que un guardián de alcance no tiene métodos. Sólo tiene un constructor y un destructor.

Por hacerlo académicamente aún más correcto podríamos impedir hacer copias de la clase MutexLocker para evitar, que una instancia afecte al mutex controlado por otra instancia. Esto podemos conseguir declarando el constructor de copia y el operador de asignación como métodos privados y sin implementarlos. De esta forma conseguiríamos un error de compilación si alguien intentara copiar una instancia de la clase MutexLocker. No obstante, es un problema académico. Normalmente nadie intenta copiar un guardián de alcance.

Trazas de entrada y salida en una función

Los guardianes de alcance se puede utilizar para más tareas. Por ejemplo, un objeto que escribe una traza cuando entramos en la función y otra cuando cuando salimos.

class TraceWriter
{
    // El constructor escribe la traza de entrada
    TraceWriter(const char* nombre_de_funcion)
    : _funcion(nombre_de_funcion)
    {
        std::cout << "Entro en " << _funcion << std::endl;
    }
    
    // El destructor escribe la traza de salida
    ~MutexLocker(void)
    {
        std::cout << "Salgo en " << _funcion << std::endl;
    }
    
    // El nombre de la función. Puede ser const, ya que no
    // se modifica durante la vida del objeto.
    const std::string _funcion;
};

// La macro que crea un objeto de la clase TraceWriter
// (si el compilador soporta la macro __FUNCTION__)
#define LOG_FUNCION  \
    TraceWriter _trace_writer(__FUNCTION__)

En una función, se llama a la macro en la primera línea

void mi_funcion
{
    LOG_FUNCION();
    // Haz algo
}

Así obtenemos una traza en stdout cuando entramos en la función y otra cuando salimos por no importe qué salida. Como usamos una macro, la podemos definir como nada en una versión de release.

Con un miembro estático en la clase TraceWriter es posible crear anidamientos. El constructor aumenta el nivel de anidamiento y el desctructor lo disminuye. Para cada nivel de anidamiento se imprime alguna cantidad de espacios delante la traza.

Memoria reservada

No olvidarse de liberar la memoria reservada es importante. Se puede utilizar la técnica descrita para reservar y liberar memoria dinámica en una función. En la biblioteca estándar de C++ ya hay clases que implementan este comportamiento:

  • unique_ptr para el estándar C++11 y
  • auto_ptr para el estándar C++0x

Presentamos aquí el uso de unique_ptr. No obstante, el código sería igual sustituyendo unique_ptr por auto_ptr.

void mi_funcion
{
    // Reserva memoria con new
    std::unique_ptr<MiClase> puntero_a_mi_objeto(new MiClase);
    
    // Haz algo con puntero_a_mi_objeto
    
    // La memoria se libera al salir de la función
}

Cuando se sale de la función, el destructor de unique_ptr llama delete puntero_a_mi_objeto. Con esto libera la memoria cuando se sale de la función.

Conclusión

Hemos visto que los “guardianes de alcance” sirven para asegurar la liberación de recursos. Además, pueden reservar y liberar estos recursos con una sóla línea en la función que los utiliza. La técnica consiste en crear una clase, cuyo constructor reserva el recurso y cuyo destructor lo libera. Esta clase suele guardar una referencia a este recurso.

Este concepto sólo se puede utilizar con los lenguajes que llaman al destructor inmediatamente después que un objeto sale del alcance. Por eso no se puede usar con lenguajes que usan un colector de basura como Java, ya que en estos lenguajes no está definido, cuando se llama al destructor. Por lo tanto no está asegurado que se libera un recurso a tiempo.

Lectura adicional

Asignar null o cero a referencias o manejadores, que no apuntan a ningún objeto válido, es una forma eficaz de evitar errores difíciles de trazear. En este artículo presento por qué es una buena idea “anular” referencias no utilizadas y como anulamos referencias para que a primera vista no parece existir un valor nulo.

Qué son referencias

En este artículo usamos el término “referencia” para varios tipos datos. Pueden ser punteros a memoria dinámica o manejadores (handles en inglés) de recursos. También pueden ser variables de objetos como en Java. Lo que todas estas entidades tienen en común es que no son un recurso sino lo representan.

Cuando se copia una referencia, hay un apuntador más al mismo recurso, pero no se copia el recurso en si. Cuando se libera un recurso, entonces todas las referencias a él se vuelven inválidos. Los problemas que pueden surgir cuando usamos referencias inválidas discutimos a continuación.

Punteros

Antes de hablar de referencias en general, quiero explicar unos conceptos básicos con el caso estrella de liarse: los punteros de C y C++. Punteros son variables que guardan una dirección en la memoria. Cuando se reserva memoria, entonces se guarda la dirección de este memoria en un puntero.

Memoria dinámica en C++

Un escenario típico para la creación de memoria dinámica es el siguiente.

// Reserva memoria - en C se haría con malloc
MiObjeto* puntero_a_objeto = new MiObjeto();

// Haz algo con los datos del puntero

// Libera la memoria
delete puntero_a_objeto;

¿Por qué el código arriba está mal? Pues, realmente no es. La memoria se reserva con new (o malloc en C) y se libera correctamente con delete (con free en C). No obstante, no se asigna NULL al puntero_a_objeto. Y esto puede causar problemas.

Por ser completos, también quiero decir, cuando no da problemas: cuando se pierde el acceso a la variable puntero_a_objeto porque se sale del bloque donde se definió. Este caso se da a menudo en destructores, donde no anular un puntero no suele causar problemas.

Apuntar al vacío

En muchas ocasiones se usa la comparación puntero == NULL para comprobar, si un puntero apunta a nada. Si no es NULL se lee y escribe en la memoria a que apunta. A veces un programa espera hasta que se haya liberado una memoria. Un puntero con el valor NULL representa esta liberación. Por lo tanto, olvidarse a anular un puntero inválido puede causar un bucle infinito. El programa espera a la liberación de la memoria, pero no se entera porque no se anuló el puntero correspondiente.

Aunque sea liberada, la memoria no desaparece. (Esto sólo pasa si sacas la memoria RAM de tu ordenador.) Como todavía existe, se puede leer de ella. Por eso, un objeto liberado puede parecer vivo y se puede emplear al menos hasta que se reserve la memoria de nuevo.

Apuntar mal

Cuando se reserva la memoria liberada de nuevo, se suele sobrescribirla con otros valores en general. Entonces pueden producirse dos casos:

Los nuevos valores no son útiles para el objeto anteriormente liberado. Por eso se lee valores malos con el puntero “olvidado” (olvidado a asignarle NULL). Un valor inútil puede ser, por ejemplo, índices de array más allá del tamaño del array. En este caso suele producirse un error en el programa en tiempo de ejecución.

El problema es encontrar este error: en el objeto anteriormente liberado parece cambiarse un índice de array así de repente y sin que exista algún instrucción correspondiente. Y ahora a adivinar por qué. El nuevo objeto que se guardó en la memoria liberada puede estar definido en una biblioteca de que ni siquiera tienes el código. Y la función en que deberías haber anulado el puntero puede estar en otro fichero.

El segundo caso es aún peor: el nuevo objeto cambia el contenido de la memoria pero los valores todavía resultan “correctos”. Por ejemplo, un índice de array vale uno en lugar de cinco. Con esto el programa puede seguir funcionando aunque más tarde o temprano algo parece estar mal.

Más tarde puede ser días o meses. ¡Imagínate que el índice apunta a un array de datos personales y de repente lees los datos de otra persona! Se envía una multa de tráfico a alguien, aunque el policía nunca lo editó así. Y todo esto por usar un puntero inválido por haberse olvidado de asignarle NULL cuando se liberó.

La regla de oro de punteros liberados

En conclusión: Si liberas memoria, asigna NULL al puntero. Sin pensar. Y aunque sea la última instrucción en un destructor. Por 99 veces que lo haces en vano, esta una que te salva de un error complicado lo vale.

Diferencias entre compilación debug y release

Para evitar desastres mayores con la memoria liberada, algunos compiladores incluyen código en modo debug, que sobrescribe la memoria liberada con algún valor tonto como 0xCCCCCCCC. Con suerte es un valor lo suficientemente inútil, para que se genere un error al usarlo de forma inmediata.

No obstante, sobrescribir cuesta tiempo y esto se quiere ahorrar. Por eso, en un programa compilado en modo release, la memoria se queda inalterada tras liberarla. Y por lo tanto se puede seguir leyendo de ella como si fuera válida. Es una de las razones porque un programa puede comportarse de forma diferente en modo debug y en modo release.

Referencias en general

La física de una referencia

Los punteros son un tipo de referencias, pero todavía representan algo físico: una dirección de memoria. Una referencia en general no está atada a una posición específica en la memoria. Sólo representa “algo” que suele estar encapsulado en una biblioteca.

Por ejemplo, cuando se crea una ventana con la biblioteca X, la función de creación devuelve un identificador que representa esta ventana. Hay que pasar este identificador como parámetro cuando se llama a una función de la biblioteca X. Dentro de la biblioteca se usa el identificador para buscar, en qué dirección de memoria se guarda la ventana y se modifica según la función llamada.

La imensa mayoría de referencias son números enteros aunque no aparecen como tales. Una variable objeto en Java o C# se comporta como un objeto de este tipo, pero internamente es un número entero que se el intérprete usa como índice en una lista de objetos.

De hecho, instancias de objetos y referencias tienen características en común. Una referencia o manejador representa una instancia de algo igual como una variable de objeto. Por ejemplo, cuando se usan sockets en en lenguaje C, se representa un socket específico por un socket id – que es un número entero. En un lenguaje orientado a objetos como Java existe una clase Socket. Se crea una conexión de socket al crear una instancia de esta clase.

Analogía entre referencias y punteros

Lo que equivale a “reservar memoria” para un puntero, es la creación de un objeto y el acceso a un recurso para una referencia o un manejador. Se llama a una función Create, que devuelve un identificador para el nuevo recurso creado, y al final se llama a una función Free para liberar este recurso.

Normalmente existe un valor que indica que una referencia o un manejador no referencia nada. En una referencia numérica suele ser cero, una variable objeto puede contener el valor null o nullptr. Tras liberar un objeto, se debería asignar este valor nulo a la referencia, ya que ahora ya no apunta a nada. Si no, las consecuencias puede ser parecidas a las de los punteros “olvidados” mencionados arriba.

Un caso real

Cuento un caso real. Con la biblioteca X Toolkit se pueden crear temporizadores. Cuando vence el tiempo establecido, la biblioteca llama a una función callback y tras esto, el temporizador ya no existe.

Una clase que usa uno de estos temporizadores guarda la referencia a ella en una variable miembro. Puede ofrecer el servicio de cancelar un temporizador. Si el valor de la variable no es cero, entonces se cancela.

Ahora nos imaginamos que el temporizador ya ha vencido y, en consecuencia ya no existe, pero no hemos anulado la referencia en la función callback. Más tarde cancelamos el temporizador. Como la variable miembro no vale cero, se cancela.

En un caso favorable, la biblioteca X Toolkit devuelve un error que este identificador ya no existe. En un caso menos favorable, el identificador, sí, existe, porque se ha vuelto a asignar para otro temporizador. Como existe, la cancelación elimina un temporizador totalmente distinto.

Como el temporizador ya no existe, ya no vencerá, y se asignará al programador correspondiente la tarea de corregir su error, cuando él no ha hecho nada mal. Y ahora imagínate que este programador eres tú y te toca tener la idea que este error puede ser el problema y, a continuación, rastrear mil ficheros de código para ver, en qué callback se olvidó anular la referencia al temporizador.

El valor NULL en cada caso

En fin, no pienses más. Si liberas un recursos, anula las referencias correspondientes. Siempre y sin pensar.

A veces no es muy obvio que es el valor nulo. Por ejemplo, ¿qué es el valor nulo para un iterador de la biblioteca estándar de C++? En este caso se le podría asignar el valor que devuelve el método end() del contenedor correspondiente.

El problema de adivinar cuál es el valor nulo suele aparecer cuando la referencia es un tipo opaco: algún tipo que devuelve una función de creación, pero que no permite asignarle el número entero cero.

Si una biblioteca no aparenta tener un valor nulo para estas referencias, entonces se puede hacer un truco. Se encapsula el identificador en una clase. Sólo es posible crear una instancia de esta clase creando una instancia del objeto que se quiere referenciar. El destructor de la clase libera el recurso. Es decir, la referencia no se anula sino desaparece con la clase que la encapsula.

Si el lenguaje no permite programación orientado a objetos, entonces se puede utilizar una variable booleana adicional, que indica si la referencia es válida o no.

Conclusión

Hemos visto, qué líos pueden surgir por referencias y punteros no anulados tras liberar el recurso a que apuntaban. Suelen causar errores que requieren mucha experiencia en fijarlos. Por eso hemos llegado a la conclusión, que siempre hay que evitar referencias inválidas asignándoles algún valor nulo. En pocos casos el valor nulo puede ser no obvio. Entonces se puede encapsular la referencia en una clase o se usa una variable booleana que indica la validez de la referencia.

Lectura adicional

Menos errores
Más estilo
Más conceptos

Métodos y funciones hacen algo. Por esta razón muchas guías de estilo exigen que sus identificadores deberían comenzar con un verbo.

Una exigencia adicional es que conviene nombrar las mismas acciones con los mismos nombres. Y la mejor forma de no olvidar es apuntárselos. Aquí presento un pequeño diccionario de verbos que puede servir como ejemplo para un diccionario corporativo.

Programar en español presenta una dificultad añadida por las flexiones del verbo, ya que se pueden usar el infinitivo o el imperativo. Lógicamente no conviene mezclar ambas formas. Cuál forma se usa es una cuestión de gusto. Usando el infinitivo tiene la ventaja que no hay plural y singular sino nada más una forma. No obstante el imperativo se ajusta más a la comprehensión humana de entender un texto, ya que uno conjuga los verbos cuando piensa en español.

Por lo tanto creo que la mejor manera no es traducir las palabras inglesas tal cual, sino usar los verbos juntos con un objeto (un objeto gramatical) para dejar más claro la conjugación empleada – que suele ser el imperativo de la segunda persona singular. Por ejemplo, en lugar de traducir “clear” con “limpia”, puede ser más comprensible emplear “limpia_memoria”. Más educado, desde luego, sería “limpie_memoria” o “limpie_memoria_por_favor”, pero tratar de usted a una máquina exige más respeto que la mayoría de los ordenadores consigue merecer. Por lo tanto, la mayoría de los programadores prefiere moverse lingüísticamente al nivel del Rey de España cuando quiere hacer callar a un presidente bolivariano, que realmente no es de Bolivia sino de Venezuela.

Para más claridad, el diccionario de verbos tiene una columna para las palabras inglesas y presenta una traducción relativamente libre del inglés al español. Palabras o partes de palabras en paréntesis son opcionales.

Dictionario de verbos
Inglés Español Uso
Apply Aplicar(Cambios) Usa los datos ya guardados en la memoria RAM para calcular los datos de salida.
Assign Asignar(Valor) “Convertir” + “Establecer”
Clear Borrar(Elementos) Limpia los contenidos. Borrar todos los elementos de un contenedor.
Close Cerrar(Flujo) Cerrar un flujo o fichero. “Cerrar” tiene semejanza con “LimpiarObjeto” en el contexto de flujos y ficheros.
Config(ure) Config(urar) Usa datos de configuración (estática) para calcular datos dependientes. Esto es similar a “Aplicar” con la conotación de sólo hacerlo una vez durante la inicialización del programa.
Construct Construir Inicializa datos de una nueva instancia. Esto es un sinónimo para “Inicializar” pero en un contexto de objetos dinámicos cuyo ciclo de vida incluye “Crear”, “Construye”, “Limpiar”, “Destruir”.
Convert Convertir Convierte datos a un tipo diferente, por ejemplo, un número a una cadena de texto.
Create Crear(Instancia) Reserva memoria para una nueva instancia. Usa “Construir” para separar inicialización y reservación de la memoria. El método “Crear” suele ser un método (estático) de una clase y no uno de objeto, ya que sirve justamente para crear este objeto.
Display Mostrar Lo mismo que “Aplicar” pero con una conotación visual. Aplicar el estado de la memoria a la presentación en pantalla.
Destroy Destruir El contrario de “Crear”. Libera memoria de un objeto dinámico. Usa “Limpiar” para separar la eliminación de datos de la desaparación del objeto.
Destruct Limpiar(Objeto) Libera recursos reservados por un objeto dinámico, pero no libera la memoria reservado por este objeto, ya que esto haría el método “Destruir” si se trabaja en un contexto de “Crear”, “Construye”, “Limpiar”, “Destruir”. Cuidado con una posible trampa lingüística: las palabras inglesas Destroy y Destruct se traducen ambas a “Destruir”.
(Get) (Obtener) Lee un dato de la memoria RAM. Se puede omitir este verbo si es posible llamar el método como el atributo correspondiente.
Init(ialize) Inicializar Asignar un valor por defecto a la memoria. Un sinónimo para “Construir” o “Configurar”.
Load Cargar Cargar un fichero entero en memoria. Abrir + Leer + Cerrar.
Open Abrir(Flujo) Abrir un flujo o fichero. “Abrir” tiene semajanza de “Crear” + “Construir” en el contexto de flujos.
Read Leer(Flujo) Leer desde un flujo o fichero.
Refresh Refrescar Un “Actualizar” gráfico. “Establecer” + “Mostrar”
Reset Reset (Reinicializar) “Limpiar” + “Inicializar”
Save Guardar Guardar un fichero entero. “Abrir” + “Escribir” + “Cerrar”
Set Establecer Guarda datos en la memoria RAM
Update Actualizar “Establecer” + “Aplicar”
Write Escribir Escribe datos a un flujo o fichero.

 

Lectura adicional

En este artículo quiero tratar como se puede usar la sentencia return para asegurar succesivamente la calidad de datos y obtener un código más limpio.

El problema

La programación estructurada se inventó para evitar el comando goto. La sentencia return también es una especie de goto ya que salta fuera de la subrutina aunque todavía queda código detrás a ejecutar. Por eso hay guías de estilo que piden que una función sólo puede tener una sentencia de return al final – normalmente para devolver el resultado de la función.

Esto es una buena regla en un cálculo lineal, es decir, cuando no se necesitan comprobar errores antes de seguir. Cálculos matemáticos son muchas veces de este tipo. La suma de a y b no falla aunque las variables contengan datos inválidos.

No obstante, en la mayoría de los casos necesitamos la comprobación de errores. En un escenario real el tratamiento de errores ocupa a menudo más espacio que el cálculo principal.

Veamos un ejemplo. Queremos leer algunos bytes de un fichero. Un programa simple tendría pocas líneas.

char buffer[1000];                       // Un buffer
FILE* f = fopen("mi_fichero.txt", "r");  // Abre fichero
fread(buffer, 1, sizeof(buffer), f);     // Lee algo
fclose(f);                               // Cierra fichero

Si tenemos en cuenta los posibles errores, la misma funcionalidad se extendería así:

char buffer[1000];   // Un buffer
bool error = false;  // Un marcador de error

// Abre fichero
FILE* f = fopen("mi_fichero.txt", "r");  

// Si podía abrir el fichero
if (f != NULL)  
{
	// Entonces lee del fichero
    const size_t read_result = 
		fread(buffer, 1, sizeof(buffer), f);
		
	// Si podía leer el fichero
    if (read_result == sizeof(buffer) 
	{
		// Haz algo
	}
	else
    {
		// Marca error si no podía leer del fichero
        error = true; 
    }
	
	// En todo caso cierra el fichero
    fclose(f);
}
else
{
	// Marca error si no podía abrir el fichero
    error = true;
}

Para cada comprobación que necesitamos para seguir adelante debemos añadir un if anidado. Esto ensucia tanto el código que podemos perder la vista sobre la esencia de la función. La idea principal de abrir, leer y cerrar un fichero queda obfuscada por tanta comprobación de error. Y este ejemplo es todavía bastante simple en comparación con un código real.

Usar múltiples return

Si permitimos el uso de varios return en medio, entonces podemos reducir el número de if anidados.

char buffer[1000];   // Un buffer
bool error = false;  // Un marcador de error

// Abre el fichero
FILE* f = fopen("mi_fichero.txt", "r");
if (f == NULL)
{
    return;
}

// Lee el fichero
const size_t read_result = 
	fread(buffer, 1, sizeof(buffer), f);
if (read_result != sizeof(buffer) 
{
    error = true;
}

// Cierra el fichero
fclose(f);

La filosofía de este procedimiento es consultar en cada paso, si los datos obtenidos son buenos y me permiten continuar. Si no puedo continuar, entonces termina la ejecución. Por ejemplo, si no encuentro el fichero en fopen, entonces no hay nada que leer y debo cancelar la función. Ya no tengo el problema de if anidados y puedo fácilmente añadir una condición más si hace falta.

Los chequeos con return en medio ayudan también a depurar el código. Si la ejecución del programa ha llegado a un cierto punto dentro de una función, entonces ya sé que todos los datos que se hayan obtenido anteriormente fueron buenos. La estructura del código me permite concentrar en cada paso del algoritmo sin dejar de hacer un chequeo exhaustivo de posibles errores.

Un return en medio de la función devuelve típicamente un código de error. Un return que devuevle un código de error notifica una excepción, es decir, un error no deseado. Por lo tanto suele ser posible lanzar una excepción en lugar de devolver un código de error. Cual de las dos formas es preferible es una decisión del diseño del programa a alto nivel y no a nivel de función. Hay lenguajes como C en que no se pueden lanzar excepciones. En otros lenguajes como Java el uso de excepciones es lo habitual.

Por cierto, para salir o saltar en bucles suele existir una sentencia equivalente al return. En los lenguaje C y Java se llaman break y continue, respectivamente. Son menos usado que el return y probablemente con razón. El enunciado return sale de la función sin más buscar. En cambio, las interrupciones de bucles saltan al inicio o al final del bucle que a veces cuesta encontrar – sobre todo en funciones largas y con bucles anidados.

Una solución curiosa

En este apartado presento una idea un poco peculiar. No recomiendo usarla y la presento sólo como una curiosidad.

La idea consiste en encadenar todas las comprobaciones en un sólo if y hacer de paso todo el trabajo.

char buffer[1000];
bool error = true;
FILE* f;
if (f = fopen("mi_fichero.txt", "r") &&
    sizeof(buffer) == fread(buffer, 1, sizeof(buffer), f))
{
    error = false;
}
if (f)
{
    fclose(f);
}

Esta construcción funciona así: se ejecuta fopen y se asigna el puntero que devuelve a f. El valor de una asignación es el valor asignado, es decir el puntero devuelto. Si este puntero vale NULL, entonces se interpreta como un false booleano. En este caso ya no se evalúa el segundo operador de la expresión && y el programa no entra en el if. En el caso que el puntero f no es nulo, se ejecuta fread y se compara con el valor esperado. Si no lo es, entonces no se entra en el if.

La condición del if sólo es cierta si todos los operadores de la expresión devuelven valores no nulos. Al primer valor nulo, se termina la evaluación de la expresión booleana y no se ejecutan las funciones siguientes. Esta forma compacta bastante el código sin dejar de comprobar errores.

Este ejemplo demuestra que alguien entiende mucho de las caracterísicas del lenguaje de programación pero quizá no tanto de la semántica del código. Muchos lenguajes como C y Java permiten asignaciones en expresiones booleanas aunque es mala práctica, ya que una expresión booleana, por semántica, es un valor y no un contenedor de datos. En cambio, este ejemplo no sólo asigna el puntero de fichero sino pone todo el código ejecutable en la condición del if.

En lenguaje humano, un uso semánticamente correcto del if sería algo como “Si lo puede hacer(condición), entonces lo hago (bloque condicional)”. El ejemplo arriba es más bien algo como “si lo hago todo, entonces bien”.

Conclusión

Hemos visto que la comprobación de errores puede afectar bastante a la claridad del código sobre el fin principal de una función. Dejando aparte la filosofía de una programación estructurada estricta podemos conseguir a reducir el nivel de anidamiento y separar el código en los pasos principales del algoritmo.

Referencias

El ejemplo de la “solución curiosa” fue inspirado en el código fuente de una implementación del algoritmo BSDIFF. Ver, por ejemplo, la función main del fichero bsdiff.cpp, líneas 260 a 264. Para descargar el archivo zip, pincha aquí.

Lectura adicional

Otras palabras claves
Técnicas el programación
Código más bonito

En este artículo quiero proponer una manera eficaz y general de suprimir una advertencia de “parámetro sin referencia” en los programas de C++. Tras explicar varias soluciones estándar, propongo la solución de la función fantasma.

Introducción

Si uno quiere programar con buen estilo, entonces suele activar el máximo nivel de advertencia del compilador. Esto, por cierto, también es una ayuda para entender bien el concepto de lenguaje. Dejarse advertir sobre todo puede resultar en que no todas las advertencias aciertan. Sin embargo, desactivarlas a coste de no ser advertido por errores significativos tampoco conviene.

Por ejemplo, si compilamos con Microsoft Visual Studio el constructor de copia

Object(const Object& source) {}

entonces se obtiene la siguiente advertencia: warning C4100: ‘source’ : parámetro formal sin referencia. Esto sería un caso típico de un constructor de copia que realmente no copia nada. Debemos mantener la interfaz para que no deje de ser un constructor de copia, pero actualmente no usamos el parámetro source.

Eliminar el parámetro

El artículo Visual C++: Warning C4100 discuta varias opciones que mencionamos aquí

  • Hacer caso a la advertencia y eliminar el parámetro no usado.
  • Si necesitamos el parámetro pero estamos seguros que no lo usaremos, entonces podemos quitar el nombre. Un constructor de copiar se quedaría sin “source”

    Object(const Object&) {}
    
  • Si queremos mantener el nombre aunque actualmente no lo usamos, podemos comentarlo.

    Object(const Object& /* source */) {}
    

    Debemos tener en cuenta que el comando @param de un comentario de documentación como lo usuaría Doxygen se quedría obsoleto. Deberíamos comentar el comantario. Además, los comentarios /* */ no suelen poder anidarse – un problema a la hora de deshabilitar bloques de código.

  • Deshabilitar la advertencia. En Microsoft Visual Studio esto se puede conseguir de la siguiente manera:

    #pragma warning( push )
    #pragma warning( disable: 4100 )
    Object(const Object& source) {}
    #pragma warning( pop )
    

    El problema de este enfoque es que se necesita una directiva #pragma diferente para cada compilador y ni siquiera está garantizado que haya. Así no conviene si el programa debe compilar en varias plataformas.

Una macro de “parámetro no referenciado”

A parte de estas propuestas suele haber otro apaño de usar el parámetro. Por ejemplo

Object(const Object& source)
{
    source = source;
}

Por ser una construcción no especialmente obvia, se puede crear una macro UNREFERENCED_PARAMETER.

#define UNREFERENCED_PARAMETER(param) param = param
Object(const Object& source)
{
    UNREFERENCED_PARAMETER(source);
}

La macro trae la ventaja que se puede definir de forma diferente según el efecto deseado. Una versión de release podría expandir la macro a nada:

#ifdef _DEBUG
#define UNREFERENCED_PARAMETER(param) param = param
#else
#define UNREFERENCED_PARAMETER(param)
#endif

Lógicamente volvería salir la advertencia cuando la macro suprime el uso del parámetro.

La desventaja de la asignación es tiempo de cálculo que requiere y que no está disponible en todas las clases. Estas clases son qquellas que tienen el operador = sobrecargado, pero ninguna sobrecarga admite una instancia de la clase misma como parámetro (o fuente) de la asignación.

Función fantasma

Mi forma preferida es una mezcla de varias propuestas y usa una función fantásma.

// La función fantásma (dummy function)
inline void reference_parameter(const void*) { }

// La macro
#define UNREFERENCED_PARAMETER(param) \
    reference_parameter(static_cast<const void*>((&param)));

// El uso
Object(const Object& source)
{
    UNREFERENCED_PARAMETER(source);
}

Esta propuesta reune todas las ventajas.

  • Gracias a referenciar el parámetro no referenciado por una macro, podemos eliminar y cambiar esta definición sin tocar el resto del código.
  • Si dejamos la macro como aquí propuesta, entonces se reemplaza por una llamada a una función con un puntero al parámetro no usado. Esta operación siempre es posible con cualquier objeto.
  • La función reference_parameter no pone nombre al parámetro, por lo cual no aparece una advertencia de “parámetro sin referencia”.
  • Finalmente es una operación rápida. Copiar un puntero es rápido y como la función no hace nada, desaparecerá con la expansión inline.

Referencias

Enlaces externos
En este sitio

Lectura adicional


Durante la modificación de código conviene a veces desactivar partes del código. La forma habitual es usar comentarios de C /* */. Esto puede traer problemas, porque estos comentarios no se puede anidar.

Una buena manera para evitar este problema es no utilizar los marcadores de comentarios /* */. Utiliza comentarios de línea simple //. La herramienta de documentación automatizada Doxygen permite marcar comentarios de documentación con una triple barra ///.

La forma más segura de desactivar código es mediante macros de compilación. Cualquier cosa que se encuentre entre #if 0 y #endif no se compila. Los entornos de desarrollo integrados modernos son capaces de marcar código excluído de la compilación con un color de fuente diferente para distinguirlo.

En lugar de #if 0 puede escribir también #if NOMBRE_DEFINIDO. Para desactivaciones no tan temporales queda más claro que un simple 0, ya que el NOMBRE_DEFINIDO puede conllevar la razón por qué se desactivó el código. Además, se puede definir este símbolo mediante una opción en la línea de comando del compilador (normalmente la opción /d) sin modificar el código.

La ventaja de las directivas respecto a los comentarios /* */ es su capacidad de anidamiento. Podemos desactivar código aunque ya contiene un trozo desactivado. (No olvides marcar con un comentarios el #endif correspondiente a cada #if)

Referencias

Introducción

“Expresiones regulares”, muchas veces abreviados por RE de regular expressions en inglés, sirven para buscar un texto dentro de otro de una forma sofisticada. Pueden servir, además, para dividir un texto complejo en componentes simples.

Un programador que domina expresiones regulares puede conseguir a dividir el código de un programa entero en símbolos con una sola llamada. No obstante, para el novato el escenario se presenta más bien al revés: puede gastarse el tiempo de escribir un programa entero para hacer bien una sola llamada con expresiones regulares.

Expresiones regulares son fascinantes y frustrantes a la vez. Este artículo trata de como podemos acercarnos al probablemente más ilegible lenguaje de programación con utilidad.

El próposito de expresiones regulares

En la programación se da muchas veces el caso de buscar una cadena de texto en otra. Por ejemplo, podemos buscar el texto “rata” en la cadena “errata”. “rata” se denomina el “patrón de búsqueda” (search pattern en inglés) o “subcadena” y “errata” el destino. (Para el destino hay más variedad de nombres.)

Si el patrón de búsqueda se encuentra dentro del texto a rastrear, es decir, si hay un match en inglés, entonces una función de búsqueda típicamente devuelve el índice de la primera ocurrencia de la subcadena (como la función strpos en PHP, o un puntero a la subcadena (como la función strstr en C).

Las funciones de búsqueda sin expresiones regulares existen también sin distinguir entre mayúsculas y minúsculas (case-insensitive en inglés) y comenzando la búsqueda desde el final – como stripos y strrpos en PHP, respectivamente.

Estas funciones son fáciles de entender, pero no ofrecen mucha flexibilidad. Por ejemplo, ¿qué hago para buscar la palabra “rata” y no simplemente la secuencia de cadena r-a-t-a dentro de cualquier palabra? Quiero una función que encuentre “rata” en “El gato caza la rata” pero no en “errata”. Podría apañarme buscando algo como espacio más “rata” más otro espacio, pero esto no funcionará si rata es la última o primera palabra. Es decir, lo que necesito es una función que encuentre mi patrón de búsqueda sólo cuando es una palabra entera y no parte de una palabra.

Pues, esta funcionalidad se obtiene con las expresiones regulares: una búsqueda con alternativas y cadenas variables en longitud y contenido.

Un ejemplo de carácter especial

Las expresiones regulares se basan en usar unos caracteres como especiales. Por ejemplo, el carácter “+” significa “el símbolo delante una o más veces. La expresión regular “ra+to” correspondería a “rato”, “raato” o “raaaaato”, pero no a “rto” (cero veces la letra a).

Hay muchos símbolo especiales que suelen estar explicados en la referencia de las funciones que usan expresiones regulares. Hay que tener en cuenta que existen pequeñas pero a veces frustrantes diferencias entre diferentes lenguajes de programación y proveedores de compiladores respecto a las expresiones regulares. Un dialecto de expresiones regulares que está muy difundido es el de perl – un lenguaje que hace mucho uso de expresiones regulares.

Componer expresiones regulares

En este artículo no quiero enumerar los códigos especiales – esto ya se hace bastante exhaustivamente en muchos manuales de referencia – sino quiero dar unas pistas de como un humano puede componer y entender un código como este:

\[\s*\w+(\s+\w+)*\s*\]

Mi forma preferida de crear una expresión regular es componerla de objetos cada vez más complejos. El código anterior corresponde en lenguaje humano a encontar “una o varias palabras entre corchetes separados o no por espacios”. Por ejemplo, algo como “[sección]”, “[índice de array]” o “[ corchetes con espacios ]”. Como una expresión regular es una cadena de texto, podemos codificarla como string.

Analizemos la definición “una o varias palabras entre corchetes separados o no por espacios”. Una palabra está compuestos por letras. El código de una letra cualquiera en una expresión regular es \w. “Uno o más” se códifica con +. Es decir, podemos decir que

const string PALABRA = "\\w+";

Aquí la barra inversa \ está duplicada porque se encuentra dentro de comillas. La mayoría de los lenguajes de programación usan la letra \ como letra de escape dentro de una cadena de texto que altera el significado de la letra siguiente. Por ejemplo, \t no es “barra más t” sino un carácter tabulador. Pues, si se quiere escribir una barra inversa, entonces la combinación es \\.

Una letra de espacio se codifica con \s. La letra * significa “cero o más”. Por lo tanto, \s* representa uno, varios o ningún espacio, es decir “separados o no por espacios”.

const string ESPACIOS_OPCIONALES = "\\s*";

Nótese que hemos no hemos denominado la constante “cero o más espacios” sino “espacios opcionales”. Es importante escoger nombres significativos pero los más cortos posibles para mejorar la calidad del código. Así conviene tener en cuenta palabras abstractas y colectivas.

De forma similar podemos codificar “uno o más espacios” con +.

const string AL_MENOS_UN_ESPACIO = "\\s+";

Como vemos, la ídea es reemplazar los códigos crípticos de las expresiones regulares por identificadores con una semántica más clara. Se pueden construir expresiones más complejas concatenando cadenas de texto, sin embargo esto no es siempre lo más práctico. Por ejemplo, es más intuitivo implementar una subexpresión regular (algo entre paréntesis) con una función.

string subexpr(string contenido)
{
    return "(" + contenido + ")";
}

“Ninguno o más” se puede implementar por

string ninguno_y_mas(string contenido)
{
    return subexpr(contenido) + "*";
}

Por mantener un mismo estilo conviene “transcribir” también construcciones que se usan menos a menudo como “en corchetes”.

string en_corchetes(string contenido)
{
    return "\\[" + contenido + "\\]";
}

Con las definiciones anteriores podemos crear expresiones más complejas como “entre espacios opcionales”

string entre_espacios_opcionales(string contenido)
{
    return ESPACIOS_OPCIONALES + contenido + ESPACIOS_OPCIONALES;
}

o varias “palabras” en lugar de una “palabra” simple

const string PALABRAS =
    PALABRA + ninguno_y_mas(AL_MENOS_UN_ESPACIO + PALABRA);

Con todo esto conseguimos que finalmente podemos construir una expresión para “Una o varias palabras entre corchetes separados o no por espacios”.

// Una o varias palabras entre corchetes separados o no por espacios
const string PALABRAS_ENTRE_CORCHETES =
	en_corchetes(entre_espacios_opcionales(PALABRAS));

Ahora la pregunta: ¿Qué es más intelligible? ¿PALABRAS_ENTRE_CORCHETES o el formato original \[\s*\w+(\s+\w+)*\s*\]?

Crítica de la composición

Una de las desventajas de componener expresiones regulares es que se puede perder la vista sobre las subexpressiones anidados. Cada función puede añadir paréntesis o no y subir el nivel de anidamiento. Normalmente esto no es un problema. Sin embargo, unas bibliotecas devuelven también resultados que se basan en estas subexpresiones – normalmente de una forma ordenada en un array. Cambiar la estructura de subexpresiones cambia el orden y la cantidad de estos subresultados. No será demasiado difícil adaptar el código si siempre se analiza la misma expresión regular, pero puede complicarse cuando la expresión es variable.

Otra desventaja es que la composición requiere un tiempo de cálculo durante la ejecución del programa. Sin embargo, esto es despreciable en la mayoría de las veces y suele ser suficiente componer una expresión regular una vez y luego guardarla en una variable estática.

La composición debe tener en cuenta los diferentes dialectos de expresiones regulares si quieren ser generales. También puede ser necesario añadir caracteres adicionales. Algunas funciones como preg_match requieren limitar una expresión regular por dos símbolos iguales como “/” + expresión + “/”. Tras el último limitador puede haber modificadores como “i” como case-insensitive.

Analizar expresiones regulares

Los ejemplos para buscar un determinado tipo de texto suelen dar las expresiones regulares tan feas como inevitablemente son. Para dar con este chorro de caracteres usamos sustituciones similares como en el apartado de composición aunque esta vez sin la necesidad de obtener un programa compilable.

Como ejemplo analizamos el ya conocido código de \[\s*\w+(\s+\w+)*\s*\] y pretendemos que aún no sabemos lo que es. Podríamos reemplazar los \s por <espacio> y los \w por <carácter>. Sin embargo es más directo si tenemos en cuenta los cuantificadores + (uno o más) y * (cero o más veces). De hecho, con un poco de práctica podemos interpretar el código de “un carácter o más” \w+ como “palabra”.

// El código original
\[\s*\w+(\s+\w+)*\s*\]

// se puede quedar en algo como
\[
<espacios opcionales>
<palabra>(<uno o más espacios><palabra>)*
<espacios opcionales>
\]

Resolvemos los paréntesis en la línea central de nuestro pseudo-código.

// La subexpresión ()* se repite entre cero y n veces
<palabra>(<uno o más espacios><palabra>)*

// se puede quedar en algo como
<palabra> cero_o_mas(<espacios><palabra>)

Una palabra más cero o más “espacios + palabra” son simplemente “palabras separadas por espacios” – popularmente conocido como “texto”. Los códigos \[ y \] son simplemente corchetes. Se deben escapear estos símbolos con \ ya que los corchetes tienen un significado especial en una expresión regular.

Con todo esto, la expresión regular dice algo como

[
<espacios opcionales>
<texto>
<espacios opcionales>
]

A esta altura ya podemos ver que la expresión \[\s*\w+(\s+\w+)*\s*\] representa a un texto entre corchetes que no necesitan estar pegados a la primera y última letra del texto.

Observaciones sobre el análisis

Como hemos visto, el análisis puede acabar en líneas largas. Si tenemos en cuenta que no necesitamos seguir ninguna sintaxis particular para el análisis de una expresión regular, entonces podemos añadir espacios o salto de líneas a nuestro gusto. Podemos copiar la expresión regular en un procesador de texto como Microsoft Word y cambiar el color de texto o la fuente si nos ayuda. El análisis es para que nosotros entendamos – no el ordenador.

Hemos hecho este análisis escribiendo, pero con un poco de práctica se puede hacer en la mente. No hay un camino de oro de como empezar. Puede convenir a reemplazar en primer lugar trozos pequeños de la expresión regular por identificadores singificativos o puede ser mejor empezar por la sustitución de subexpresiones entre paréntesis. Lo más importante es que no dejemos de ver el bosque por tantos árboles y escogemos los identificadores adecuados. El código \w+ no es “uno o más caracteres” sino una “palabra”.

Conclusión

Como expresiones regulares no permiten espacios y saltos de línea como elemento de estructuración, se hacen difícil a leer y entender. He propuesto una manera de componer y analizar expresiones regulares que transforma símbolos crípticos a conceptos humanos.

Hay muchos artículos sobre qué es una expresión regular pero hasta ahora no he visto ningún artículo similar para no perderse en ellas. Un poco me sorprende porque mi enfoque me ha ayudado bastante y creo todo el mundo tiene problemas similares a la hora de iniciarse en las expresiones regulares.

De hecho podría ser un avance de crear un lenguaje de expresiones regulares estructuradas que permite escribir patrones de búsqueda en un formato más legible para los humanos y que se dejaría transformar fácilmente en las expresiones regulares habituales.

Referencias

Páginas externos
En este mismo sitio

Ya escribí sobre los conceptos básicos de una simulación numérica en el artículo Cómo hacer una simulación numérica. En este artículo quiero hablar de un caso más específico que muchas veces se da en la física: la simulación de una ecuación diferencial.

La “solución” de una ecuación diferencial sería una expresión analítica como x(t) = algo que ya no depende ni de la función x(t) ni de una de sus derivadas. Hallar estas soluciones es un arte difícil; a veces demasiado difícil. Esto puede llegar a tanta desperación que se ha llegado a ofrecer una recompensa de un millón de dólares para dar la solución de la ecuación diferencial de Navier-Stokes – una cantidad decente para adquirir el equipo informático necesario para simularla.

Como ejemplo en este artículo usamos el movimiento de cuerpos celestes bajo el efecto de la gravedad. Este movimiento se puede describir en una ecuación diferencial. Es posible resolver esta ecuación diferencial para dos cuerpos y, debido a esto, se puede predicir con gran exactitud la posición de un planeta que gira alrededor del sol. Sin embargo, todavía no se ha hallado una solución exacta del problema de tres cuerpos.

Mientras no se puede hallar una ecuación analíticamente, no queda más remedio que simularla. Una simulación se suele hacer sobre el tiempo, es decir, se conoce un estado para un momento determinado y se calcula el estado para un instante después.

Para ello se hace uso de una característica fundamental de las ecuaciones diferenciales: que sean diferenciables – es decir que describen curvas sin salto ni esquinas. Cuando más de cerca veo una curva diferenciable, más se parece a una recta. Por ejemplo, si miro un círculo con una lupa, entonces sólo veo una parte del círculo. Cuando más pequeña es esta parte, más recta parece. Bajo un microscopio ya será difícil decidir si veo una parte de una recta o de un círculo. Esto sginifica, en matemático, que una aproximación de una curva por secciones rectas cortas es lo suficientemente buena cuando las secciones son lo suficientemente cortas. Esto resulta en eligir pasos lo suficientemente pequeños para una simulación numérica. Por simular sobre el tiempo, estos pasos son pasos de tiempo.

Cuando estos pasos son lo suficientemente pequeños, entonces puedo aproximar el problema por ecuaciones líneales. Por ejemplo, durante un instante corto T, el vuelo de la Tierra alrededor del sól sigue una recta. Durante este tiempo corto, la acceleración hacia el sol es constante. El algoritmo para calcular el movimiento de la Tierra o cualquier otro cuerpo celeste sería el siguiente.

  1. Para el momento n conozco la masa, la posición y las velocidades de todos los puntos de masa. Calculo la aceleración an que un cuerpo experimienta con la ley de Newtown, que es la suma vectorial de todas las acceleraciones gravitatorias que sufre este cuerpo por todos las demás masas.
  2. Considero la aceleración an constante durante un paso de tiempo T. Con este paso de tiempo calculo la velocidad: un+1 = an · T
  3. Considero también la velocidad un+1 constante durante un paso de tiempo T. Así hallo la nueva posición: xn+1 = un · T
  4. Con estos calculos conozco todos los datos para el instante n+1 y puedo comenzar una nueva iteración del algoritmo para el instante n+2 desde el punto 1.

Una simulación numérica es una aproximación. Debido a esto y la resolución numérica limitada de los ordenadores se acumulan errores significativos durante la simulación. Por ejemplo, los cuerpos podrían alcanzar velocidades cada vez mayores con el paso de tiempo – algo que no sucedería en la realidad. Por eso es habitual añadir factores de corrección. En el ejemplo podríamos forzar que la energía de movimiento total sea constante reduciendo o aumentando conjuntamiente las velocidades de todos los puntos de masa.

Suele requerir experiencia encontrar la mejor manera de “estabilizar” la simulación numérica, es decir, conseguir que se ajuste a la realidad física durante un tiempo idealmente infinito. A veces se hace un análisis matemático exhaustivo para entender los errores sistemáticos, por ejemplo, por qué la energía total tiene tendencia de aumentar. Al final no se hacen las simulaciones numéricas porque son fáciles, sino porque hallar la solución de la ecuación diferencial fue imposible.

Otra manera de mejorar la calidad de una simulación es variar la longitud de paso de tiempo. Cuando más corto es, más se aproxima el resultado a la realidad, pero menos tiempo total simulo con el mismo esfuerzo. Idealmente puedo ajustar la longitud del paso del tiempo según la situacición. Por ejemplo, la aceleración entre dos puntos de masa alejados entre sí no varia mucho y, por lo tanto, no cometo mucho error a tomar la aceleración constante. En cambio, en un momento drámatico de acercamiento veloz, la dirección y magnitud de la aceleración cambian rápidamente. En esta situación debo acortar el paso de tiempo si quiero mantener la misma magnitud de error en el cálculo.

A veces conviene transformar una ecuación diferencial en otra forma más ventajosa para la simulación numérica. Conseguir que encima el algoritmo de una simulación sea estable requiere a menudo un gran conocmiento matemático. Sin embargo, las versiones más simples y menos exactos son a menudo la base para video-juegos o animaciones bastante impresionantes. Por eso son también un reto acequible para aprendices en programacion.

Como no existe un “completamente correcto” en una simulación numérica, son una herramienta de enseñanza ideal, cuando el camino es la meta y no el resultado final. La programación de ecuaciones diferenciales es un camino en que se puede aprender mucha programación.

Referencias

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

Únete a otros 50 seguidores

Archivos

octubre 2017
L M X J V S D
« Ene    
 1
2345678
9101112131415
16171819202122
23242526272829
3031