Voy a empezar mi serie de artículos sobre metaprogramación con el preprocesador de C. El preprocesador de C es un lenguaje independiente a C que se ejecuta en un paso previo a la compilación. De ahí su nombre de “preprocesador” (procesa el código (procesador) antes (pre)). Es un lenguaje sencillo que permite transformar partes de un código en otro. No dispone de variables y su sintaxis rompe por completo con la sintaxis de C (a diferencia de los otros tipos de metaprogramación). El hecho de tener dos sintaxis diferentes y una generación de código en ocasiones tremendamente difíciles de seguir, es algo que a la larga se consideró un gran problema y se evitó, (al menos en parte) en futuros lenguajes.

Hablaré un poco por encima sobre cómo funciona el preprocesador de C. Aunque si queréis información detallada, adjunto algunas referencias al final del artículo…

En el preprocesador de C las sentencias o declaraciones, comienzan por #.

Inclusión de archivos:

La forma en la que soluciona C la reutiliazción de código en diferentes módulos es mediante #include. Los includes tienen numerosos problemas y los lenguajes de programación serios evitan el uso de includes y usan el concepto de importación de módulos.

  • PHP tiene includes (mal)
  • D, Java, C#, python tienen módulos (bien)
#include "file_in_c_folder.h"  
#include <file_in_one_included_folder.h>  

Los includes permiten inyectar código en cualquier parte del código. Desde en la cabecera (útil para inclusión de archivos de cabecera .h) que por convención contienen definiciones de funciones y símbolos externos, hasta en el cuerpo de funciones para inyectar código que podamos condicionar mediante macros. Lo que por cierto, es una guarrada.

¿Qué pasa cuando una cabecera incluye otras cabeceras y esas cabeceras a más cabeceras? ¿Qué pasa si se repite algún include?

En C si se declara un símbolo varias veces, es un error.

En C para evitar la inclusión de la misma cabecera en varias ocasiones, se hace uso de macros (que veremos más adelante). Envolviendo el código con un #ifndef (que también veremos más adelante).

#ifndef _MY_FILE_H  
#define_MY_FILE_H  
// Código de la cabecera  
#endif  

Algunos compiladores soportan #pragma once. Pero al no ser soportado por todos, no se suele usar cuando se quiere código portable.

El apaño del #ifndef hace que el preprocesado sea bastante lento, por la cantidad de código que se puede acabar teniendo que preprocesar. Aunque algunos compiladores hacen optimizaciones de estos casos para evitar.

Ejecución condicional

#if - If expresión  
#elif - else if expresión  
#else - else  
#ifdef - if defined  
#ifndef - if not defined  
#endif - end if  
#ifdef MY_C_COMPILER  
    #pragma(special, value)  
#endif  
#ifdef POSIX  
    #include <sys/time.h>  
#endif  

Macros:

En el preprocesador de C hay símbolos/macros. Los/las símbolos/macros contienen una expresión a reemplazar y pueden tener parámetros que se pueden usar en el reemplazo.
Si no tienen parámetros som “object-like macros”. Y si tienen parámetros son “function-like macros”.

Definición de macros:

Las macros se pueden definir mediante parámetros al ejecutar el compilador:

gcc -Dsymbol_without_value  
gcc -Dsymbol_with_value=3  
cl /Dsymbol_without_value  
cl /Dsymbol_with_value=3  

Las macros se pueden definir en el propio código:

#define M_PI 3.141592  
#define MAX(a, b) (((a) > (b)) ? (a) : (b))  

Las expresiones de las macros no tienen por qué ser expresiones válidas solas:

#define VMPARAM_END , VM* vm  
#define ODD_NUMBERS_FROM_7_TO_11_HEAD 7, 8, 9, 10, 11,  

Las macros pueden usar otras macros:

#define MIN_WIDTH 320  
#define MAX_WIDTH 640  
#define FINAL_WIDTH(width) MAX(MIN_WIDTH, MIN(width, MAX_WIDTH)) 

También se pueden eliminar:

#undef main  

Macros predefinidas y especiales:

Hay ciertas macros especiales que ya están definidas. Cada compilador define algunas, y puede llegar a parecer un zoológico de la variedad y poca consistencia que hay. Para más detalles:
http://gcc.gnu.org/onlinedocs/cpp/Predefined-Macros.html

Definidas por el compilador:
Todos los compiladores de C definen una serie de macros que permiten identificar el compilador y la plataforma en la que estamos. Como cada uno lo hace a su manera, hay un cacao monumental en cuanto a las macros que están definidas o no.

Macros de posicionamiento:
Hay un par de macros especiales que tienen un valor variable (dependiente del archivo y de la línea donde se ejecuten): LINE y FILE respectivamente.

Convertir una expresión en una cadena mediante #param:

#define PRINT_EXPRESSION(value) printf("%s", #value)  
PRINT_EXPRESSION(1 + 3);  
==> printf("%s", "1 + 3");  

Concatenar un parámetro con un identificador:

#define DECL_FUNC(FUNC_NAME) func_ ## FUNC_NAME ## _id  
DECL_FUNC(test)  
==> func_test_id  

Macros con parámetros variables:

#define eprintf(...) fprintf(stderr, __VA_ARGS__);  
#define eprintf(args...) fprintf(stderr, args);  
#define eprintf(format, ...) fprintf(stderr, format, __VA_ARGS__);  

Macros multilínea (añadiendo ‘' al final de línea excepto en la última):

#define REINTERPRET(TYPE, VALUE) *((TYPE) *)&(VALUE)  
#define VARTYPE_SET_VALUE(TYPE, VALUE) \  
    void SET_ ## TYPE (VARTYPE *value) { \  
         (*value)->type = TYPE_ ## TYPE; \  
         (*value)->value = REINTERPRET(TYPE, VALUE); \  
    }  

Casos prácticos:

Suelo usar C en muy pocas ocasiones, y cada vez menos. La metaprogramación mediante el preprocesador de C es muy poco flexible en comparación a los traits + mixins + funciones puras ejecutadas en tiempo de compilación y reflexión y enguarra el código una barbaridad.

En cualquier caso, el preprocesador de C se utiliza para ejecutar código más rápido y de forma más genérica. Supongamos el siguiente caso:

int max(int a, int b) {  
    return (a > b) ? a : b;  
}  

#define MAX_VALUE 7  

void main() {  
    printf("%d", max(10, MAX_VALUE));  
}  

En este código la función max no se hace inline. Y aunque se puede especificar el modificador inline o __inline (y que no siempre puede dar los resultados esperados), no tiene tipado genérico (solo sirve para enteros y no para enteros largos o floats)

#define max(a, b) (((a) > (b)) ? (a) : (b))  

Permite cualquier tipo que soporte el operador “>”. Los valores constantes se podrán optimizar por el compilador con mayor facilidad.

Aunque puede llevar a resultados inesperados por la ejecución de múltiples veces de uno de los parámetros. Y son todas estas cosas, las que hacen el preprocesado de C una mala elección.
http://code.google.com/p/phpmedia/source/browse/#svn%2Ftrunk%2Fsrc
http://code.google.com/p/vnvm/source/browse/trunk#trunk%2Fsrc

Referencias: