Ricerca…


introduzione

Il preprocessore C è un semplice parser / replacer di testo che viene eseguito prima della compilazione effettiva del codice. Utilizzato per estendere e facilitare l'uso del linguaggio C (e successivo C ++), può essere utilizzato per:

un. Compresi altri file che utilizzano #include

b. Definisci una macro di sostituzione del testo usando #define

c. Compilazione condizionale usando #if #ifdef

d. Logica specifica per piattaforma / compilatore (come estensione della compilazione condizionale)

Osservazioni

Le istruzioni di preprocessore vengono eseguite prima che i file di origine vengano consegnati al compilatore. Sono capaci di una logica condizionale di livello molto basso. Poiché i costrutti del preprocessore (ad es. Macro simili a oggetti) non vengono digitati come normali funzioni (la fase di pre-elaborazione avviene prima della compilazione), il compilatore non può imporre controlli di tipo, pertanto dovrebbero essere usati con attenzione.

Includi le guardie

Un file di intestazione può essere incluso da altri file di intestazione. Un file sorgente (unità di compilazione) che include più intestazioni può quindi, indirettamente, includere alcune intestazioni più di una volta. Se un tale file di intestazione che è incluso più di una volta contiene definizioni, il compilatore (dopo la pre-elaborazione) rileva una violazione della regola di una definizione (ad esempio §3.2 dello standard C ++ 2003) e pertanto genera una diagnostica e la compilazione non riesce.

L'inclusione multipla è impedita usando "include guardie", che a volte sono anche conosciute come guardie di intestazione o macro guardie. Questi sono implementati usando il preprocessore #define , #ifndef , direttive #endif .

per esempio

// Foo.h
#ifndef FOO_H_INCLUDED 
#define FOO_H_INCLUDED

class Foo    //  a class definition
{
};

#endif

Il vantaggio principale dell'utilizzo di protezioni include è che funzioneranno con tutti i compilatori e i preprocessori conformi agli standard.

Tuttavia, le guardie includono anche alcuni problemi per gli sviluppatori, in quanto è necessario garantire che le macro siano uniche all'interno di tutte le intestazioni utilizzate in un progetto. Nello specifico, se due (o più) intestazioni utilizzano FOO_H_INCLUDED come guardia di inclusione, la prima di quelle intestazioni incluse in una unità di compilazione impedirà efficacemente che gli altri vengano inclusi. Particolari sfide vengono introdotte se un progetto utilizza un numero di librerie di terze parti con i file di intestazione che capita di utilizzare guardie in comune.

È inoltre necessario assicurarsi che le macro utilizzate in protezioni non siano in conflitto con altre macro definite nei file di intestazione.

La maggior parte delle implementazioni C ++ supporta anche la direttiva #pragma once che garantisce che il file venga incluso solo una volta all'interno di una singola compilation. Questa è una direttiva standard di fatto , ma non fa parte di alcuno standard ISO C ++. Per esempio:

// Foo.h
#pragma once

class Foo
{
};

Mentre #pragma once evita alcuni problemi associati alle guardie, un #pragma - per definizione negli standard - è intrinsecamente un hook specifico del compilatore, e verrà ignorato silenziosamente dai compilatori che non lo supportano. I progetti che utilizzano #pragma once sono più difficili da trasferire ai compilatori che non lo supportano.

Un certo numero di linee guida di codifica e standard di garanzia per C ++ in particolare scoraggiano qualsiasi uso del preprocessore diverso da #include file di intestazione o allo scopo di inserire protezioni incluse nelle intestazioni.

Logica condizionale e gestione multipiattaforma

In breve, la logica di pre-elaborazione condizionale riguarda la possibilità di rendere la logica del codice disponibile o non disponibile per la compilazione utilizzando le definizioni di macro.

Tre casi d'uso importanti sono:

  • diversi profili di app (ad es. debug, release, test, ottimizzati) che possono essere candidati alla stessa app (ad esempio con registrazione aggiuntiva).
  • cross-platform compila - codice di base singola, piattaforme di compilazione multiple.
  • utilizzando una base di codice comune per più versioni dell'applicazione (es. versioni Basic, Premium e Pro di un software) - con caratteristiche leggermente diverse.

Esempio a: un approccio multipiattaforma per la rimozione dei file (illustrativo):

#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
}

Macro come _WIN32 , __APPLE__ o __unix__ sono normalmente predefinite dalle corrispondenti implementazioni.

Esempio b: abilitazione della registrazione aggiuntiva per una build di debug:

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"
}

Esempio c: abilitare una funzione premium in una build di prodotto separata (nota: questo è illustrativo, spesso è un'idea migliore consentire di sbloccare una funzionalità senza la necessità di reinstallare un'applicazione)

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
}

Alcuni trucchi comuni:

Definizione dei simboli al momento del richiamo:

Il preprocessore può essere richiamato con simboli predefiniti (con inizializzazione opzionale). Ad esempio questo comando ( gcc -E esegue solo il preprocessore)

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

elabora Sample.cpp nello stesso modo in cui sarebbe se #define OPTIMISE_FOR_OS_X e #define TESTING_MODE 1 venissero aggiunti all'inizio di Sample.cpp.

Garantire una macro è definita:

Se una macro non è definita e il suo valore viene confrontato o verificato, il preprocessore assume quasi sempre in silenzio il valore di 0 . Ci sono alcuni modi per lavorare con questo. Un approccio è quello di assumere che le impostazioni predefinite siano rappresentate come 0 e che qualsiasi modifica (ad esempio al profilo di build dell'app) debba essere esplicitamente eseguita (ad es. ENABLE_EXTRA_DEBUGGING = 0 per impostazione predefinita, set -DENABLE_EXTRA_DEBUGGING = 1 per eseguire l'override). Un altro approccio è rendere esplicite tutte le definizioni e le impostazioni predefinite. Questo può essere ottenuto utilizzando una combinazione di direttive #ifndef e #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

Macro

Le macro sono suddivise in due gruppi principali: macro simili a oggetti e macro simili a funzioni. Le macro vengono considerate come sostituzione di token all'inizio del processo di compilazione. Ciò significa che sezioni di codice grandi (o ripetute) possono essere astratte in una macro di preprocessore.

// 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 libreria Qt utilizza questa tecnica per creare un sistema meta-oggetto facendo in modo che l'utente dichiari la macro Q_OBJECT all'inizio della classe definita dall'utente che estende QObject.

I nomi macro sono in genere scritti in maiuscolo, per renderli più facili da distinguere dal codice normale. Questo non è un requisito, ma è semplicemente considerato di buon stile da molti programmatori.


Quando viene rilevata una macro simile a un oggetto, viene espansa come una semplice operazione di copia-incolla, con il nome della macro sostituito con la sua definizione. Quando viene rilevata una macro simile a una funzione, vengono espansi sia il nome che i relativi parametri.

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))

A causa di ciò, i parametri macro simili a funzioni sono spesso racchiusi tra parentesi, come in AREA() sopra. Questo serve a prevenire eventuali bug che possono verificarsi durante l'espansione della macro, in particolare i bug causati da un singolo parametro macro composto da più valori effettivi.

#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));

Si noti inoltre che, a causa di questa semplice espansione, è necessario prestare attenzione con i parametri passati ai macro, per prevenire effetti collaterali imprevisti. Se il parametro viene modificato durante la valutazione, verrà modificato ogni volta che viene utilizzato nella macro espansa, che di solito non è ciò che vogliamo. Questo è vero anche se la macro racchiude i parametri tra parentesi per impedire all'espansione di rompere qualsiasi cosa.

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

Inoltre, le macro non forniscono alcuna sicurezza del tipo, con conseguenti errori di difficile comprensione relativi alla mancata corrispondenza dei tipi.


Poiché i programmatori normalmente terminano le righe con un punto e virgola, i macro che si intendono utilizzare come linee indipendenti sono spesso progettati per "inghiottire" un punto e virgola; questo impedisce che eventuali bug non intenzionali siano causati da un punto e virgola in più.

#define IF_BREAKER(Func) Func();

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

In questo esempio, il doppio punto e virgola accidentale interrompe il blocco if...else , impedendo al compilatore di far corrispondere il else al if . Per evitare ciò, il punto e virgola viene omesso dalla definizione della macro, che lo farà "inghiottire" il punto e virgola immediatamente dopo ogni suo utilizzo.

#define IF_FIXER(Func) Func()

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

Lasciare il punto e virgola finale consente inoltre di utilizzare la macro senza terminare l'istruzione corrente, il che può essere utile.

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

// ...

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

Normalmente, una definizione di macro termina alla fine della riga. Se una macro deve coprire più righe, tuttavia, una barra rovesciata può essere utilizzata alla fine di una riga per indicare ciò. Questo backslash deve essere l'ultimo carattere della riga, che indica al preprocessore che la riga seguente deve essere concatenata sulla riga corrente, trattandoli come una singola riga. Questo può essere usato più volte di seguito.

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

// ...

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

Ciò è particolarmente utile in macro di funzioni complesse, che potrebbero dover coprire più righe.

#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'.")

Nel caso di macro più simili alle funzioni, può essere utile assegnare loro il proprio ambito per evitare possibili conflitti di nomi o causare la distruzione di oggetti alla fine della macro, in modo simile a una funzione effettiva. Un idioma comune per questo è do mentre 0 , dove la macro è racchiusa in un blocco do-while . Generalmente questo blocco non viene seguito con un punto e virgola, consentendogli di ingoiare un punto e virgola.

#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);

Esistono anche macro variadiche; analogamente alle funzioni variadiche, questi prendono un numero variabile di argomenti e quindi li espandono tutti al posto di un parametro speciale "Varargs", __VA_ARGS__ .

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

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

Nota che durante l'espansione, __VA_ARGS__ può essere posizionato ovunque nella definizione e verrà espanso correttamente.

#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);

Nel caso di un parametro variadico a zero argomenti, diversi compilatori gestiranno diversamente la virgola finale. Alcuni compilatori, come Visual Studio, ingoiano silenziosamente la virgola senza alcuna sintassi speciale. Altri compilatori, come GCC, richiedono di posizionare ## immediatamente prima di __VA_ARGS__ . A causa di ciò, è saggio definire condizionalmente le macro variadiche quando la portabilità è un problema.

// 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 */

Messaggi di errore del preprocessore

Gli errori di compilazione possono essere generati usando il preprocessore. Ciò è utile per una serie di motivi, tra cui alcuni, che includono la notifica a un utente se si trovano su una piattaforma non supportata o su un compilatore non supportato.

es. Errore di restituzione se la versione di gcc è 3.0.0 o precedente.

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

es. Errore di restituzione in caso di compilazione su un computer Apple.

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

Macro predefinite

Le macro predefinite sono quelle definite dal compilatore (diversamente da quelle definite dall'utente nel file sorgente). Queste macro non devono essere ridefinite o indefinite dall'utente.

Le seguenti macro sono predefinite dallo standard C ++:

  • __LINE__ contiene il numero di riga della linea su cui è utilizzata questa macro e può essere modificato dalla direttiva #line .
  • __FILE__ contiene il nome file del file in cui questa macro è utilizzata e può essere modificata dalla direttiva #line .
  • __DATE__ contiene la data (nel formato "Mmm dd yyyy" ) della compilazione del file, dove Mmm è formattato come se fosse ottenuto da una chiamata a std::asctime() .
  • __TIME__ contiene il tempo (nel formato "hh:mm:ss" ) della compilazione del file.
  • __cplusplus è definito da compilatori C ++ (conformi) durante la compilazione di file C ++. Il suo valore è la versione standard che il compilatore è pienamente conforme, ad esempio 199711L per C ++ 98 e C ++ 03, 201103L per C ++ 11 e 201402L per C ++ 14 standard.
c ++ 11
  • __STDC_HOSTED__ è definito su 1 se l'implementazione è ospitata o 0 se è indipendente .
c ++ 17
  • __STDCPP_DEFAULT_NEW_ALIGNMENT__ contiene un letterale size_t , che è l'allineamento utilizzato per una operator new chiamata all'operatore di allineamento non consapevole.

Inoltre, le seguenti macro possono essere predefinite dalle implementazioni e possono o non possono essere presenti:

  • __STDC__ ha un significato dipendente dall'implementazione e viene generalmente definito solo quando si compila un file come C, per indicare la piena conformità allo standard C. (O mai, se il compilatore decide di non supportare questa macro.)
c ++ 11
  • __STDC_VERSION__ ha un significato dipendente dall'implementazione, e il suo valore è solitamente la versione C, analogamente a quanto __cplusplus sia la versione C ++. (O non è nemmeno definito, se il compilatore decide di non supportare questa macro.)
  • __STDC_MB_MIGHT_NEQ_WC__ è definito a 1 , se i valori della codifica ristretta del set di caratteri di base potrebbero non essere uguali ai valori delle loro controparti ampie (ad esempio se (uintmax_t)'x' != (uintmax_t)L'x' )
  • __STDC_ISO_10646__ è definito se wchar_t è codificato come Unicode e si espande in una costante intera nel formato yyyymmL , indicando l'ultima revisione Unicode supportata.
  • __STDCPP_STRICT_POINTER_SAFETY__ è definito su 1 , se l'implementazione ha una sicurezza puntatore rigorosa (altrimenti ha una sicurezza di puntatore rilassata )
  • __STDCPP_THREADS__ è definito su 1 , se il programma può avere più di un thread di esecuzione (applicabile all'implementazione indipendente - le implementazioni ospitate possono sempre avere più di un thread)

Vale anche la pena ricordare __func__ , che non è una macro, ma una variabile locale predefinita. Contiene il nome della funzione in cui è utilizzato, come array di caratteri statici in un formato definito dall'implementazione.

Oltre a quelle macro predefinite standard, i compilatori possono avere il proprio set di macro predefinite. Si deve fare riferimento alla documentazione del compilatore per apprenderli. Per esempio:

Alcune delle macro sono solo per richiedere il supporto di alcune funzionalità:

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

Altri sono molto utili per il debug:

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;
}

E altri per il controllo della versione banale:

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";
    }
}

X-macro

Una tecnica idiomatica per generare strutture di codice ripetitive in fase di compilazione.

Una X-macro consiste di due parti: la lista e l'esecuzione della lista.

Esempio:

#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;
}

che è espanso dal preprocessore nel seguente:

Animal dog;
Animal cat;
Animal racoon;

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

    return 0;
}    

Man mano che le liste diventano più grandi (diciamo, più di 100 elementi), questa tecnica aiuta ad evitare un eccesso di copia-incolla.

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

Vedi anche: X-macros


Se la definizione di una X seamingly irrilevante prima di usare LIST non è di tuo gradimento, puoi anche passare un nome di macro come argomento:

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

Ora, si specifica esplicitamente quale macro deve essere utilizzata quando si espande l'elenco, ad es

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

Se ogni invocazione della MACRO dovesse assumere parametri aggiuntivi - costante rispetto all'elenco, è possibile utilizzare macro 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__))

Il primo argomento è fornito dal LIST , mentre il resto è fornito dall'utente nel richiamo LIST . Per esempio:

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

si espanderà a

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

#pragma una volta

La maggior parte, ma non tutte, le implementazioni C ++ supportano la direttiva #pragma once che garantisce che il file venga incluso solo una volta all'interno di una singola compilation. Non fa parte di alcuno standard ISO C ++. Per esempio:

// Foo.h
#pragma once

class Foo
{
};

Mentre #pragma once evita alcuni problemi associati alle guardie , un #pragma - per definizione negli standard - è intrinsecamente un hook specifico del compilatore, e verrà ignorato silenziosamente dai compilatori che non lo supportano. I progetti che utilizzano #pragma once devono essere modificati per essere conformi agli standard.

Con alcuni compilatori, in particolare quelli che utilizzano intestazioni precompilate , #pragma once può comportare una notevole accelerazione del processo di compilazione. Allo stesso modo, alcuni preprocessori ottengono l'accelerazione della compilazione rintracciando quali intestazioni hanno impiegato le guardie. Il vantaggio netto, quando sono impiegati entrambi #pragma once e include guardie, dipende dall'implementazione e può essere un aumento o una diminuzione dei tempi di compilazione.

#pragma once combinato con le protezioni incluse, era il layout consigliato per i file di intestazione quando si scrivevano applicazioni basate su MFC su Windows, ed era generato dalla add class Visual Studio, add dialog , add windows procedure guidate di add windows . Quindi è molto comune trovarli combinati in C ++ Windows Candidati.

Operatori preprocessori

# operatore o operatore di stringa viene utilizzato per convertire un parametro Macro in una stringa letterale. Può essere utilizzato solo con le macro con argomenti.

// 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");

Il compilatore concatena due stringhe e l'argomento finale printf() sarà una stringa letterale con carattere di fine riga alla fine.

Il preprocessore ignorerà gli spazi prima o dopo l'argomento macro. Quindi sotto la dichiarazione di stampa ci darà lo stesso risultato.

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

Se il parametro della stringa letterale richiede una sequenza di escape come prima di una doppia citazione (), verrà automaticamente inserito dal preprocessore.

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

## operatore o operatore di Token che incolla è usato per concatenare due parametri o token di 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);

e l'output finale sarà

variableY = 15


Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow