C++
preprocessore
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 astd::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 esempio199711L
per C ++ 98 e C ++ 03,201103L
per C ++ 11 e201402L
per C ++ 14 standard.
-
__STDC_HOSTED__
è definito su1
se l'implementazione è ospitata o0
se è indipendente .
-
__STDCPP_DEFAULT_NEW_ALIGNMENT__
contiene un letteralesize_t
, che è l'allineamento utilizzato per unaoperator 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.)
-
__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 a1
, 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 sewchar_t
è codificato come Unicode e si espande in una costante intera nel formatoyyyymmL
, indicando l'ultima revisione Unicode supportata. -
__STDCPP_STRICT_POINTER_SAFETY__
è definito su1
, se l'implementazione ha una sicurezza puntatore rigorosa (altrimenti ha una sicurezza di puntatore rilassata ) -
__STDCPP_THREADS__
è definito su1
, 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:
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