Buscar..


Introducción

El preprocesador de C es un simple analizador / sustituto de texto que se ejecuta antes de la compilación real del código. Usado para extender y facilitar el uso del lenguaje C (y posterior C ++), puede usarse para:

a. Incluyendo otros archivos usando #include

segundo. Defina una macro de reemplazo de texto usando #define

do. Compilación condicional usando #if #ifdef

re. Lógica específica de la plataforma / compilador (como una extensión de la compilación condicional)

Observaciones

Las instrucciones del preprocesador se ejecutan antes de que los archivos de origen se entreguen al compilador. Son capaces de lógica condicional de muy bajo nivel. Dado que las construcciones del preprocesador (p. Ej., Macros similares a objetos) no se escriben como las funciones normales (el paso de preprocesamiento ocurre antes de la compilación), el compilador no puede imponer verificaciones de tipos, por lo tanto, deben usarse con cuidado.

Incluir guardias

Un archivo de encabezado puede ser incluido por otros archivos de encabezado. Por lo tanto, un archivo fuente (unidad de compilación) que incluye múltiples encabezados puede, indirectamente, incluir algunos encabezados más de una vez. Si tal archivo de encabezado que se incluye más de una vez contiene definiciones, el compilador (después del preprocesamiento) detecta una violación de la Regla de una definición (por ejemplo, §3.2 del estándar C ++ 2003) y, por lo tanto, emite un diagnóstico y falla la compilación.

La inclusión múltiple se evita utilizando "incluir guardas", que a veces también se conocen como guardas de encabezado o guardas de macros. Estos se implementan utilizando las directivas #define , #ifndef , #endif del preprocesador.

p.ej

// Foo.h
#ifndef FOO_H_INCLUDED 
#define FOO_H_INCLUDED

class Foo    //  a class definition
{
};

#endif

La ventaja clave del uso de incluir guardias es que funcionarán con todos los compiladores y preprocesadores que cumplen con los estándares.

Sin embargo, incluir protecciones también causa algunos problemas para los desarrolladores, ya que es necesario asegurarse de que las macros sean únicas en todos los encabezados utilizados en un proyecto. Específicamente, si dos (o más) encabezados usan FOO_H_INCLUDED como su protección de inclusión, el primero de esos encabezados incluidos en una unidad de compilación evitará efectivamente que se incluyan los demás. Se presentan desafíos particulares si un proyecto usa una cantidad de bibliotecas de terceros con archivos de encabezado que se usan para incluir guardas en común.

También es necesario asegurarse de que las macros utilizadas en las guardas de inclusión no entren en conflicto con ninguna otra macros definida en los archivos de encabezado.

La mayoría de las implementaciones de C ++ también admiten la directiva #pragma once , que garantiza que el archivo solo se incluya una vez en una única compilación. Esta es una directiva estándar de facto , pero no es parte de ningún estándar ISO C ++. Por ejemplo:

// Foo.h
#pragma once

class Foo
{
};

Si bien #pragma once evita algunos problemas asociados con la inclusión de guardias, un #pragma , por definición en los estándares, es inherentemente un gancho específico del compilador y será ignorado silenciosamente por los compiladores que no lo admiten. Los proyectos que usan #pragma once son más difíciles de trasladar a compiladores que no lo admiten.

Una serie de pautas de codificación y estándares de seguridad para C ++ desalientan específicamente cualquier uso del preprocesador que no sea para #include los archivos de encabezado o con el propósito de incluir guardas en los encabezados.

Lógica condicional y manejo multiplataforma.

En pocas palabras, la lógica de preprocesamiento condicional consiste en hacer que la lógica de código esté disponible o no esté disponible para la compilación utilizando definiciones de macros.

Tres casos de uso prominentes son:

  • diferentes perfiles de aplicación (por ejemplo, depuración, lanzamiento, prueba, optimizado) que pueden ser candidatos de la misma aplicación (por ejemplo, con registro adicional).
  • multiplataforma compila - sola base de código, compilación de múltiples plataformas.
  • utilizando una base de código común para múltiples versiones de aplicaciones (por ejemplo, versiones Basic, Premium y Pro de un software), con características ligeramente diferentes.

Ejemplo a: un enfoque multiplataforma para eliminar archivos (ilustrativo):

#ifdef _WIN32
#include <windows.h> // and other windows system files
#endif
#include <cstdio>

bool remove_file(const std::string &path) 
{
#ifdef _WIN32
  return DeleteFile(path.c_str());
#elif defined(_POSIX_VERSION) || defined(__unix__)
  return (0 == remove(path.c_str()));
#elif defined(__APPLE__)
  //TODO: check if NSAPI has a more specific function with permission dialog
  return (0 == remove(path.c_str()));
#else 
#error "This platform is not supported"
#endif
}

Las macros como _WIN32 , __APPLE__ o __unix__ normalmente están predefinidas por las implementaciones correspondientes.

Ejemplo b: habilitar el registro adicional para una compilación de depuración:

void s_PrintAppStateOnUserPrompt()
{
    std::cout << "--------BEGIN-DUMP---------------\n"
              << AppState::Instance()->Settings().ToString() << "\n"
#if ( 1 == TESTING_MODE ) //privacy: we want user details only when testing
              << ListToString(AppState::UndoStack()->GetActionNames())
              << AppState::Instance()->CrntDocument().Name() 
              << AppState::Instance()->CrntDocument().SignatureSHA() << "\n"
#endif
              << "--------END-DUMP---------------\n"
}

Ejemplo c: habilite una función premium en una compilación de producto separada (nota: esto es ilustrativo. A menudo es una mejor idea permitir que una función se desbloquee sin la necesidad de reinstalar una aplicación)

void MainWindow::OnProcessButtonClick()
{
#ifndef _PREMIUM
    CreatePurchaseDialog("Buy App Premium", "This feature is available for our App Premium users. Click the Buy button to purchase the Premium version at our website");
    return;
#endif
    //...actual feature logic here
}

Algunos trucos comunes:

Definición de símbolos en el momento de invocación:

Se puede llamar al preprocesador con símbolos predefinidos (con inicialización opcional). Por ejemplo, este comando ( gcc -E ejecuta solo el preprocesador)

gcc -E -DOPTIMISE_FOR_OS_X -DTESTING_MODE=1 Sample.cpp

procesa Sample.cpp de la misma manera que lo haría si #define OPTIMISE_FOR_OS_X y #define TESTING_MODE 1 se agregaran a la parte superior de Sample.cpp.

Asegurando una macro se define:

Si una macro no está definida y su valor se compara o verifica, el preprocesador casi siempre asume que el valor es 0 . Hay algunas maneras de trabajar con esto. Un enfoque consiste en asumir que la configuración predeterminada se representa como 0, y cualquier cambio (por ejemplo, en el perfil de compilación de la aplicación) debe realizarse explícitamente (por ejemplo, ENABLE_EXTRA_DEBUGGING = 0 por defecto, establecer -DENABLE_EXTRA_DEBUGGING = 1 para anular). Otro enfoque es hacer que todas las definiciones y valores predeterminados sean explícitos. Esto se puede lograr usando una combinación de #error #ifndef y #error :

#ifndef (ENABLE_EXTRA_DEBUGGING)
// please include DefaultDefines.h if not already included.
#    error "ENABLE_EXTRA_DEBUGGING is not defined"
#else
#    if ( 1 == ENABLE_EXTRA_DEBUGGING )
  //code
#    endif
#endif

Macros

Las macros se clasifican en dos grupos principales: macros similares a objetos y macros similares a funciones. Las macros se tratan como una sustitución de token al principio del proceso de compilación. Esto significa que grandes secciones (o repetidas) de código se pueden abstraer en una macro preprocesadora.

// This is an object-like macro
#define    PI         3.14159265358979

// This is a function-like macro.
// Note that we can use previously defined macros
// in other macro definitions (object-like or function-like)
// But watch out, its quite useful if you know what you're doing, but the
// Compiler doesnt know which type to handle, so using inline functions instead
// is quite recommended (But e.g. for Minimum/Maximum functions it is quite useful)
#define    AREA(r)    (PI*(r)*(r))

// They can be used like this:
double pi_macro   = PI;
double area_macro = AREA(4.6);

La biblioteca Qt utiliza esta técnica para crear un sistema de metaobjetos al hacer que el usuario declare la macro Q_OBJECT al frente de la clase definida por el usuario que extiende QObject.

Los nombres de macro generalmente se escriben en mayúsculas, para que sean más fáciles de diferenciar del código normal. Esto no es un requisito, pero muchos programadores lo consideran un buen estilo.


Cuando se encuentra una macro similar a un objeto, se expande como una simple operación de copiar y pegar, con el nombre de la macro reemplazado con su definición. Cuando se encuentra una macro similar a una función, tanto su nombre como sus parámetros se expanden.

double pi_squared = PI * PI;
// Compiler sees:
double pi_squared = 3.14159265358979 * 3.14159265358979;

double area = AREA(5);
// Compiler sees:
double area = (3.14159265358979*(5)*(5))

Debido a esto, los parámetros de macros similares a funciones a menudo se incluyen entre paréntesis, como en AREA() anterior. Esto es para evitar cualquier error que pueda ocurrir durante la expansión de la macro, específicamente los errores causados ​​por un solo parámetro de macro compuesto por múltiples valores reales.

#define BAD_AREA(r) PI * r * r

double bad_area = BAD_AREA(5 + 1.6);
// Compiler sees:
double bad_area = 3.14159265358979 * 5 + 1.6 * 5 + 1.6;

double good_area = AREA(5 + 1.6);
// Compiler sees:
double good_area = (3.14159265358979*(5 + 1.6)*(5 + 1.6));

También tenga en cuenta que debido a esta simple expansión, se debe tener cuidado con los parámetros pasados ​​a las macros, para evitar efectos secundarios inesperados. Si el parámetro se modifica durante la evaluación, se modificará cada vez que se use en la macro expandida, que generalmente no es lo que queremos. Esto es cierto incluso si la macro incluye los parámetros entre paréntesis para evitar que la expansión rompa algo.

int oops = 5;
double incremental_damage = AREA(oops++);
// Compiler sees:
double incremental_damage = (3.14159265358979*(oops++)*(oops++));

Además, las macros no proporcionan seguridad de tipos, lo que lleva a errores difíciles de entender sobre la falta de coincidencia de tipos.


Como los programadores normalmente terminan las líneas con un punto y coma, las macros que están diseñadas para usarse como líneas independientes a menudo están diseñadas para "tragar" un punto y coma; esto evita que cualquier error involuntario sea causado por un punto y coma adicional.

#define IF_BREAKER(Func) Func();

if (some_condition)
    // Oops.
    IF_BREAKER(some_func);
else
    std::cout << "I am accidentally an orphan." << std::endl;

En este ejemplo, el punto y coma doble inadvertido rompe el bloque if...else , impidiendo que el compilador haga coincidir el else con el if . Para evitar esto, el punto y coma se omite en la definición de macro, lo que provocará que se "trague" el punto y coma inmediatamente después de su uso.

#define IF_FIXER(Func) Func()

if (some_condition)
    IF_FIXER(some_func);
else
    std::cout << "Hooray!  I work again!" << std::endl;

Dejar el punto y coma final también permite que la macro se use sin terminar la declaración actual, lo que puede ser beneficioso.

#define DO_SOMETHING(Func, Param) Func(Param, 2)

// ...

some_function(DO_SOMETHING(some_func, 3), DO_SOMETHING(some_func, 42));

Normalmente, una definición de macro termina al final de la línea. Sin embargo, si una macro necesita cubrir varias líneas, se puede usar una barra invertida al final de una línea para indicar esto. Esta barra invertida debe ser el último carácter de la línea, lo que indica al preprocesador que la siguiente línea debe estar concatenada en la línea actual, tratándolas como una sola línea. Esto se puede utilizar varias veces seguidas.

#define TEXT "I \
am \
many \
lines."

// ...

std::cout << TEXT << std::endl; // Output:   I am many lines.

Esto es especialmente útil en macros complejas similares a funciones, que pueden necesitar cubrir múltiples líneas.

#define CREATE_OUTPUT_AND_DELETE(Str) \
    std::string* tmp = new std::string(Str); \
    std::cout << *tmp << std::endl; \
    delete tmp;

// ...

CREATE_OUTPUT_AND_DELETE("There's no real need for this to use 'new'.")

En el caso de macros similares a funciones más complejas, puede ser útil darles su propio alcance para evitar posibles colisiones de nombres o para destruir objetos al final de la macro, similar a una función real. Un lenguaje común para esto es do mientras 0 , donde la macro está encerrada en un bloque do-while . Este bloque generalmente no tiene un punto y coma, lo que le permite tragar un punto y coma.

#define DO_STUFF(Type, Param, ReturnVar) do { \
    Type temp(some_setup_values); \
    ReturnVar = temp.process(Param); \
} while (0)

int x;
DO_STUFF(MyClass, 41153.7, x);

// Compiler sees:

int x;
do {
    MyClass temp(some_setup_values);
    x = temp.process(41153.7);
} while (0);

También hay macros variadic; de manera similar a las funciones variadic, estas toman un número variable de argumentos y luego los expanden todos en lugar de un parámetro especial "Varargs", __VA_ARGS__ .

#define VARIADIC(Param, ...) Param(__VA_ARGS__)

VARIADIC(printf, "%d", 8);
// Compiler sees:
printf("%d", 8);

Tenga en cuenta que durante la expansión, __VA_ARGS__ se puede colocar en cualquier lugar de la definición y se expandirá correctamente.

#define VARIADIC2(POne, PTwo, PThree, ...) POne(PThree, __VA_ARGS__, PTwo)

VARIADIC2(some_func, 3, 8, 6, 9);
// Compiler sees:
some_func(8, 6, 9, 3);

En el caso de un parámetro variadic de cero argumentos, diferentes compiladores manejarán la coma final de manera diferente. Algunos compiladores, como Visual Studio, tragarán silenciosamente la coma sin ninguna sintaxis especial. Otros compiladores, como GCC, requieren que coloque ## inmediatamente antes de __VA_ARGS__ . Debido a esto, es aconsejable definir condicionalmente macros variables cuando la portabilidad es una preocupación.

// In this example, COMPILER is a user-defined macro specifying the compiler being used.

#if       COMPILER == "VS"
    #define VARIADIC3(Name, Param, ...) Name(Param, __VA_ARGS__)
#elif     COMPILER == "GCC"
    #define VARIADIC3(Name, Param, ...) Name(Param, ##__VA_ARGS__)
#endif /* COMPILER */

Mensajes de error del preprocesador

Los errores de compilación se pueden generar utilizando el preprocesador. Esto es útil por varias razones, algunas de las cuales incluyen, notificar a un usuario si están en una plataforma no compatible o en un compilador no compatible.

por ejemplo, Devolver error si la versión de gcc es 3.0.0 o anterior.

#if __GNUC__ < 3
#error "This code requires gcc > 3.0.0"
#endif

por ejemplo, Devolver error si se compila en una computadora Apple.

#ifdef __APPLE__
#error "Apple products are not supported in this release"
#endif

Macros predefinidas

Las macros predefinidas son aquellas que define el compilador (en contraste con las definidas por el usuario en el archivo fuente). Esas macros no deben ser redefinidas o no definidas por el usuario.

Las siguientes macros están predefinidas por el estándar C ++:

  • __LINE__ contiene el número de línea de la línea en la que se utiliza esta macro, y puede modificarse mediante la directiva #line .
  • __FILE__ contiene el nombre de archivo del archivo en el que se usa esta macro y puede modificarse mediante la directiva #line .
  • __DATE__ contiene la fecha (en formato "Mmm dd yyyy" ) de la compilación del archivo, donde Mmm se formatea como si se hubiera obtenido mediante una llamada a std::asctime() .
  • __TIME__ contiene el tiempo (en formato "hh:mm:ss" ) de la compilación del archivo.
  • __cplusplus es definido por compiladores C ++ (conformes) mientras compila archivos C ++. Su valor es la versión estándar con la que el compilador es totalmente 199711L , es decir, 199711L para C ++ 98 y C ++ 03, 201103L para C ++ 11 y 201402L para C ++ 14 estándar.
c ++ 11
  • __STDC_HOSTED__ se define como 1 si la implementación está alojada , o 0 si es independiente .
c ++ 17
  • __STDCPP_DEFAULT_NEW_ALIGNMENT__ contiene un literal size_t , que es la alineación utilizada para una llamada al operator new desconoce la alineación.

Además, las siguientes macros pueden ser predefinidas por las implementaciones y pueden estar o no presentes:

  • __STDC__ tiene un significado que depende de la implementación, y generalmente se define solo cuando se compila un archivo como C, para indicar el cumplimiento del estándar C completo. (O nunca, si el compilador decide no admitir esta macro).
c ++ 11
  • __STDC_VERSION__ tiene un significado que depende de la implementación, y su valor suele ser la versión C, de manera similar a como __cplusplus es la versión C ++. (O incluso no está definido, si el compilador decide no admitir esta macro).
  • __STDC_MB_MIGHT_NEQ_WC__ se define como 1 , si los valores de la codificación restringida del conjunto de caracteres básicos podrían no ser iguales a los valores de sus contrapartes amplias (por ejemplo, si (uintmax_t)'x' != (uintmax_t)L'x' )
  • __STDC_ISO_10646__ se define si wchar_t está codificado como Unicode y se expande a una constante entera en la forma yyyymmL , que indica la última revisión de Unicode compatible.
  • __STDCPP_STRICT_POINTER_SAFETY__ se define como 1 , si la implementación tiene una seguridad de puntero estricta (de lo contrario tiene una seguridad de puntero relajada )
  • __STDCPP_THREADS__ se define como 1 , si el programa puede tener más de un subproceso de ejecución (aplicable a la implementación independiente; las implementaciones hospedadas siempre pueden tener más de un subproceso)

También vale la pena mencionar __func__ , que no es una macro, sino una función predefinida variable local. Contiene el nombre de la función en la que se utiliza, como una matriz de caracteres estáticos en un formato definido por la implementación.

Además de esas macros predefinidas estándar, los compiladores pueden tener su propio conjunto de macros predefinidas. Uno debe referirse a la documentación del compilador para aprenderlos. P.ej:

Algunas de las macros son solo para consultar el soporte de alguna característica:

#ifdef __cplusplus // if compiled by C++ compiler
extern "C"{ // C code has to be decorated
   // C library header declarations here
}
#endif

Otros son muy útiles para la depuración:

c ++ 11
bool success = doSomething( /*some arguments*/ );
if( !success ){
    std::cerr << "ERROR: doSomething() failed on line " << __LINE__ - 2
              << " in function " << __func__ << "()"
              << " in file " << __FILE__
              << std::endl;
}

Y otros para control de versiones triviales:

int main( int argc, char *argv[] ){
    if( argc == 2 && std::string( argv[1] ) == "-v" ){
        std::cout << "Hello World program\n"
                  << "v 1.1\n" // I have to remember to update this manually
                  << "compiled: " << __DATE__ << ' ' << __TIME__ // this updates automagically
                  << std::endl;
    }
    else{
        std::cout << "Hello World!\n";
    }
}

Macros x

Una técnica idiomática para generar estructuras de código repetidas en tiempo de compilación.

Una X-macro consta de dos partes: la lista y la ejecución de la lista.

Ejemplo:

#define LIST \
    X(dog)   \
    X(cat)   \
    X(racoon)

// class Animal {
//  public:
//    void say();
// };

#define X(name) Animal name;
LIST
#undef X

int main() {
#define X(name) name.say();
    LIST
#undef X

    return 0;
}

el cual es expandido por el preprocesador en lo siguiente:

Animal dog;
Animal cat;
Animal racoon;

int main() {
    dog.say();
    cat.say();
    racoon.say();

    return 0;
}    

A medida que las listas se vuelven más grandes (digamos, más de 100 elementos), esta técnica ayuda a evitar el pegado excesivo de copias.

Fuente: https://en.wikipedia.org/wiki/X_Macro

Ver también: X-macros


Si definir una X irrelevante para la costura antes de usar LIST no es de su agrado, también puede pasar un nombre de macro como argumento:

#define LIST(MACRO) \
    MACRO(dog) \
    MACRO(cat) \
    MACRO(racoon)

Ahora, especifica explícitamente qué macro se debe usar al expandir la lista, por ejemplo,

#define FORWARD_DECLARE_ANIMAL(name) Animal name;
LIST(FORWARD_DECLARE_ANIMAL)

Si cada invocación de la MACRO debe tomar parámetros adicionales - constante con respecto a la lista, se pueden usar macros variadic

//a walkaround for Visual studio
#define EXPAND(x) x

#define LIST(MACRO, ...) \
    EXPAND(MACRO(dog, __VA_ARGS__)) \
    EXPAND(MACRO(cat, __VA_ARGS__)) \
    EXPAND(MACRO(racoon, __VA_ARGS__))

El primer argumento lo proporciona LIST , mientras que el resto lo proporciona el usuario en la invocación LIST . Por ejemplo:

#define FORWARD_DECLARE(name, type, prefix) type prefix##name;
LIST(FORWARD_DECLARE,Animal,anim_)
LIST(FORWARD_DECLARE,Object,obj_)

se expandirá a

Animal anim_dog;
Animal anim_cat;
Animal anim_racoon;
Object obj_dog;
Object obj_cat;
Object obj_racoon;        

#pragma una vez

La mayoría, pero no todas, las implementaciones de C ++ son compatibles con la directiva #pragma once , que garantiza que el archivo solo se incluya una vez en una única compilación. No es parte de ninguna norma ISO C ++. Por ejemplo:

// Foo.h
#pragma once

class Foo
{
};

Si bien #pragma once evita algunos problemas asociados con la inclusión de guardias , un #pragma , por definición en los estándares, es inherentemente un gancho específico del compilador y será ignorado silenciosamente por los compiladores que no lo admiten. Los proyectos que usan #pragma once deben modificarse para cumplir con los estándares.

Con algunos compiladores, particularmente aquellos que emplean encabezados precompilados , #pragma once puede resultar en una considerable aceleración del proceso de compilación. De manera similar, algunos preprocesadores logran una aceleración de la compilación al rastrear qué encabezados han empleado incluyen guardias. El beneficio neto, cuando se utilizan tanto #pragma once como incluir guardias, depende de la implementación y puede ser un aumento o una disminución de los tiempos de compilación.

#pragma once combinado con incluir guardas, fue el diseño recomendado para los archivos de encabezado al escribir aplicaciones basadas en MFC en Windows, y fue generado por la add class Visual Studio, el add dialog add windows asistentes de add windows . Por lo tanto, es muy común encontrarlos combinados en los solicitantes de Windows en C ++.

Operadores de preprocesador

# operador # o el operador de cadena se usa para convertir un parámetro de Macro a un literal de cadena. Solo se puede utilizar con las macros que tienen argumentos.

// preprocessor will convert the parameter x to the string literal x
#define PRINT(x) printf(#x "\n")

PRINT(This line will be converted to string by preprocessor);
// Compiler sees
printf("This line will be converted to string by preprocessor""\n");

El compilador concatena dos cadenas y el argumento final printf() será una cadena literal con un carácter de nueva línea al final.

El preprocesador ignorará los espacios antes o después del argumento de la macro. Así que debajo de la declaración impresa nos dará el mismo resultado.

PRINT(   This line will be converted to string by preprocessor );

Si el parámetro de la cadena literal requiere una secuencia de escape como antes de una comilla doble (), el preprocesador lo insertará automáticamente.

PRINT(This "line" will be converted to "string" by preprocessor); 
// Compiler sees
printf("This \"line\" will be converted to \"string\" by preprocessor""\n");

## operador ## o el operador de pegado de token se utiliza para concatenar dos parámetros o tokens de una macro.

// preprocessor will combine the variable and the x
#define PRINT(x) printf("variable" #x " = %d", variable##x)

int variableY = 15;
PRINT(Y);
//compiler sees
printf("variable""Y"" = %d", variableY);

y la salida final será

variableY = 15


Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow