jun
29

En los últimos artículos sobre las Macros en C/C++ hemos explicado de una forma un tanto rápida todo el potencial que presenta esta herramienta si sabemos utilizarla con un nivel medio-alto en determinadas situaciones.

Uno de los problemas que se nos presentan en ocasiones a la hora de trabajar en un lenguaje como C es que, a pesar de que contamos con depuradores de código, no siempre tenemos uno a mano, por lo que si a la vez que documentamos podemos mostrar a nuestro capricho y en función de la situación un log de lo que está sucediendo durante su ejecución, podremos hacernos una idea bastante aproximada de si todo va bien o no.

Tras esta pequeña reflexión me lanzo a compartir con vosotros un pequeño fichero .h que nos facilitará enormemente la tarea en una 20 líneas y que he utilizado ya en el desarrollo de varios programas:


#include <stdio.h>
//#define DEBUG 0
#define DEBUG 1
#ifndef __LOG_H
#define __LOG_H
 #if DEBUG == 1
 #define log(format, args...) \
		fprintf (stderr, "%s - %i: ", __FILE__, __LINE__);\
		fprintf (stderr, format "\n", ##args);
 #define warning(format, args...) \
			log("/!\\ "format" /!\\", ##args)
#define error(format, args...) \
		log("(X) "format" (X)", ##args)
 #else
#define log(format, args...) ;
#define warning(format, args...) ;
#define error(format, args...) ;
 #endif
#endif

Este fichero tendremos que incluirlo en nuestro programa escrito en C mediante una directiva #include:

#include "log.h"

en este caso he llamado al fichero que contiene esas 20 líneas de código.

De manera que si tenemos declarado DEBUG con un valor de 0 no se mostrará salida alguna y no sólo eso, sino que las líneas desaparecerán (es un claro ejemplo de uso de macros, ya que en nuestro programa en producción puede que deseemos tener más velocidad de procesado y no nos interese interactuar con salidas ni llamar a funciones que no hagan nada), mientras que si tiene un valor de 1, escupirá toda la información que deseemos por, en este caso, la salida de error de nuestro programa.

Si ejecutamos el siguiente programa en C:


#include "log.h"
int main(int argc, char** argv){

log("asdf%i", 1)

log("asdf")

warning("esto es una alerta")

warning("esto es una alerta%i", 1)

error("esto es un error")

error("esto es un error%i", 1)

}

obtendremos la siguiente salida:

bottle.c – 4: asdf1

bottle.c – 5: asdf

bottle.c – 6: /!\ esto es una alerta /!\

bottle.c – 7: /!\ esto es una alerta1 /!\

bottle.c – 8: (X) esto es un error (X)

bottle.c – 9: (X) esto es un error1 (X)

La ausencia de puntos y coma después de llamar las macros no es un error, es que simplemente no hacen falta.

Finalmente y a modo aclaratorio, las constantes utilizadas en el fichero log.h __FILE__ y __LINE__ son dos directivas de preprocesador que nos indican en qué lína y fichero de código tenemos la llamada a ese log.

Categoría C/C++, Producción propia, Programación | Sé el primero en comentar!
jun
22

Como último artículo de la serie, vamos a adentrarnos un poco más en la declaración de elementos similares a las funciones con macros , para ser más exactos, con sus argumentos y posibilidades:

Convertir a String

En ocasiones no es de un gran interés tratar de la misma manera todos los argumentos que le pasamos a este tipo de funciones de preprocesador.

En el caso concreto de que deseemos tratar el argumento como un String constante, habremos de precederlo con el símbolo ‘#‘, demnaera que estamos convirtiendo en constantes dichos parámetros, pero veamos un ejemplo que he sacado de gnu.org, ya que me parece bastante ilustrativo.

#define WARN_IF(EXP) \
     do { if (EXP) \
             fprintf (stderr, "Warning: " #EXP "\n"); } \
     while (0)
     WARN_IF (x == 0);

Este pedazo de código nos dará una salida como la siguiente:

do { if (x == 0) fprintf (stderr, “Warning: ” “x == 0″ “\n”); } while (0);

Es decir, el argumento EXP se sustituye como argumento en el primer caso y como un string en el segundo.

Una de las grandes ventajas de la conversión a String de este tipo de macros, es que no sólo añade dobles comillas, sino que escapa los caracteres necesarios para que la cadena se muestre tal y como es, a no ser que esta se encuentre dentro de una cadena de texto ya o sea un carácter como ‘\n‘.

Finalmente puede resultarnos de utilidad concatenar un par de tokens dados dentro de una macro; para ello podremos hacer uso de ‘##

Parámetros indefinidos

Algo que puede resultarnos muy interesante e incluso increíble cuando empezamos a utilizar C, es que, si bien nos han enseñado a que las funciones tienen un número definido de parámetros, ¿cómo es posible la existencia de funciones como printf?

Al igual que para las funciones, existe una forma de hacer que una macro acepte un número no definido de parámetros, y este método aparece en forma de __VA_ARGS__. Este token nos permitirá sustotuir en nuestro macro los ‘‘ por los valores que se le han pasado a la función.

#define log(...) printf(__VA_ARGS__)

Si queremos ser más descriptivos con los parámetros podemos sustituir ‘‘ por ‘<nombre>…‘, por ejemplo:

#define log(args...) printf(args)

Si queremos especificar un número mínimo de argumentos, podemos hacerlo de la siguiente manera:

#define log(formato, args...) printf(formato, args)

pero esto hace que sea obligatorio, en todo momento, si no pasar un segundo argumento, sí escribir la coma después del primero. Para solucionar este problema, de nuevo acudimos al manual de gnu.org que nos dice que echemos mano de ‘##‘, de manera que podamos omitir, si lo deseamos el segundo parámetro y la coma de después del primero al llamar a la macro:

#define log(formato, args...) printf(formato, ##args)
Categoría C/C++, Manuales | Sé el primero en comentar!
jun
15

En el pasado y primer artículo de esta serie, describimos a grosso modo el funcionamiento de la directiva de preprocesador #define; siendo una de las características más importantes a tener en cuenta el hecho de que a la hora de compilar el programa no estamos trabajando con variables con un valor, sino con los valores directamente. De este modo estaremos ahorrando memoria y dotando de legibilidad a nuestro código en determinadas ocasiones.

Hoy vamos a ver cómo simular funciones con el sistema de Macros. Antes de nada, hay que aclarar que: si bien para constantes, por convenio, se utilizan letras mayúsculas en todo momento, para esta especie de funciones no es necesario hacer esto, de manera que podemos continuar con el sistema de nomenclaturas que hemos estado llevando hasta el momento (como recomendación, yo usaría todo en minúsculas menos la primera letra de cada palabra exceptuando la de la primera, Ex: hallarMaximo(…)).

Supongamos que contamos con dos funciones en nuestras librerías:

int rand(void);
int fastRand(void);

Ambas funciones llevan a cabo la misma tarea, sólo que utilizan diferentes métodos para realizarla, de hecho, la segunda que es mucho más eficiente que la primera (como puede entenderse por el nombre), la acabamos de implementar y, a pesar de que la entrada y la salida es la misma, la primera ha sido utilizada cientos de veces en nuestro código, y cambiarla implicaría un repaso exhaustivo del código fuente y, en caso de tener una API pública, implicaría cambiarla y notificárselo a todos nuestros usuarios.

En lugar de esto, podemos crear una macro que se encargue de realizar dicha sustitución durante el proceso de precompilado:

#define rand() fastRand()

de manera que, cada vez que llamemos a la función rand() en nuestro código, a la hora de compilarlo, es como si estuviésemos llamando a la función fastRand().

Pero el poder de esta herramienta no se para con estas características, sino que nos permite definir algo parecido a funciones de forma completa. Con esto quiero decir que podemos definir macros que a la hora de editar el código y utilizarlos, podemos asumir que son funciones. Pongamos un ejemplo:

#define max(x, y) ((x > y) ? x : y)

una vez hehcha esta definición, nosotros, desde nuestro código podremos llamar a esta “función” evitando que se gasten, entre otras cosas niveles de pila, ya que el código es sustituido durante el proceso de preprocesado, de manera que si escribimos en nuestro código:

int a = 2;
int b = 4;
int z = max(a, b);

el código que realmente se estará ejecutando una vez compilado será:

int a = 2;
int b = 4;
int z = ((a > b) ? a : b);

Uno de los grandes problemas de utilizar esta técnica para sustituir a las funciones es que, como podemos observar es que, a la hora de llamar a la macro, no tenemos comprobación del tipado de los datos que le pasamos, pudiendo obtener así, resultados inesperados o incluso errores de estabilidad del programa. Hay que ser conscientes en todo momento de que los argumentos, a la hora de llamar a una macro que se comporta por una función, estarán separados por comas.

Finalmente, a la hora de llamar a una macro, si no le pasamos el número de argumentos necesarios, obtendremos un error de preprocesador, pero en caso de que le pasemos argumentos vacíos, estos se sustiuirán por argumentos vacíos sin dar error durante el proceso de preprocesado.

Categoría C/C++, Manuales | Sé el primero en comentar!
jun
1

Todo programador de C y C++ que se precie debe estar más o menos familiarizado con las directivas de preprocesador en general y mas concretamente, debería estarlo con las llamadas Macros. Una macro es un trozo de código, usualmente en la zona de cabecera o en los propios ficheros de cabecera (*.h).

Las macros se definen mediante la directiva #define; veamos un ejemplo:

#define PI 3.14159

Esta macro se encarga de asociar el valor 3.14159 con el valor PI mediante la directiva #define, es decir, la estructura sería algo así como lo siguiente:

#define <alias> <valor>

dónde <alias> es el nombre que queremos utilizar para referirnos al término que ocuparía el lugar de <valor>. Ésta característica de C se utiliza sobre todo para facilitar el uso de variables que durante todo el programa van a tener el mismo valor, ya sea por el hecho de que son constantes, como por ejemplo el valor del número pi (3.14159), el valor de la gravedad terrestre g (9.81) o cualquier otra constante o porque simplmente son eso, constantes que no variarán, como por ejemplo el tamaño máximo de un array.

Un aspecto relativamente importante de las macros es que, por convenio, y para facilitar la lectura de las mismas en programas, se suelen utilizar letras mayúsclas, de manera que si nos encontramos con:

int main(int argc, char** argv){

printf("%f, %f", r, PI);

return 0;

}

podríamos concluir que la letra r se corresponde con una variable, ya que está está (o empieza) con minúsculas, mientras que PI es un valor constante. Ambos valores han de estar definidos con anterioridad.

Veamos un ejemplo práctico de uso:


#include <stdio.h>

#define PI 3.14159

int main(int argc, char** argv){

double radio = 25.684;

double area = PI * radio * radio;

return 0;

}

La directiva #define no sólo nos servirá para definir valores de variables, sino que también podremos utilizarlo para declarar valores de arrays:

#define CONTENIDO 0, \
                     1, \
                     2
int main(int argc, char** argv){
int valores[] = { CONTENIDO };
return 0;
}

Como habréis podido comprobar detrás de las dos primeras líneas de código aparece una barra inclinada (\), esto quiere decir que la declaración de la macro va a continuar en la línea siguiente, lo cual nos facilitará enormemente la legibilidad de nuestro código.

¿Por qué nos convendría usar macros?

El uso de macros puede parecer un poco absurdo de buenas a primeras, ya que podemos utilizar funciones (ya veremos más adelante el porqué de esta afirmación) o variables para conseguir los mismos resultados. La diferencia entre usar unas u otras es que al usar macros, el código del <alias> se sustituye por el del <valor> durante el proceso de pre-compilación, de manera que a la hora de compilarlo, el código queda sustituido y al ejecutarlo no tendremos que malgastar ciclos de CPU realizando cálculos como este.

¿Podemos invalidar algo declarado con un #define?

Efectivamente podemos hacerlo mediante el uso de la directiva de preprocesador:

#undef <alias>
Categoría C/C++ | Sé el primero en comentar!