C++
Preprocessor
Sök…
Introduktion
C-förbehandlaren är en enkel textperserare / ersättare som körs före den faktiska sammanställningen av koden. Används för att utöka och underlätta användningen av språket C (och senare C ++), kan det användas för:
a. Inklusive andra filer med #include
b. Definiera en makro för #define
med #define
c. Villkorlig sammanställning med #if
#ifdef
d. Plattform / Compiler-specifik logik (som en förlängning av villkorlig sammanställning)
Anmärkningar
Förberedande uttalanden körs innan dina källfiler överlämnas till kompilatorn. De kan ha villkorad logik på mycket låg nivå. Eftersom förbehandlarkonstruktioner (t.ex. objektliknande makron) inte skrivs som normala funktioner (förbehandlingssteget sker före kompilering) kan kompilatorn inte genomföra typkontroller, de bör därför användas noggrant.
Inkludera vakter
En rubrikfil kan inkluderas av andra rubrikfiler. En källfil (kompilationsenhet) som innehåller flera rubriker kan därför indirekt innehålla vissa rubriker mer än en gång. Om en sådan rubrikfil som ingår mer än en gång innehåller definitioner, upptäcker kompilatorn (efter förbehandling) en överträdelse av One Definition-regeln (t.ex. §3.2 i 2003 C ++ -standarden) och utfärdar därför en diagnos och kompilering misslyckas.
Flera inkludering förhindras med användning av "inkludera vakter", som ibland också kallas rubrikvakter eller makroskydd. Dessa implementeras med hjälp av förbehandlardirektiven #define
, #ifndef
och #endif
.
t.ex
// Foo.h
#ifndef FOO_H_INCLUDED
#define FOO_H_INCLUDED
class Foo // a class definition
{
};
#endif
Den viktigaste fördelen med att använda inkludera skydd är att de kommer att arbeta med alla standardkompatibla kompilatorer och förbehandlare.
Dock inkluderar skydd också vissa problem för utvecklare, eftersom det är nödvändigt att se till att makronen är unika inom alla rubriker som används i ett projekt. Specifikt, om två (eller fler) rubriker använder FOO_H_INCLUDED
som sitt skydd, kommer den första av de rubriker som ingår i en sammanställningsenhet effektivt att förhindra att de andra ingår. Särskilda utmaningar införs om ett projekt använder ett antal tredjepartsbibliotek med rubrikfiler som råkar använda inkluderar vakter gemensamt.
Det är också nödvändigt att se till att makronen som används i inkluderar vakter inte står i konflikt med några andra makron som definieras i rubrikfiler.
De flesta C ++ #pragma once
stöder också #pragma once
direktivet en #pragma once
som säkerställer att filen endast ingår en gång i en enda sammanställning. Detta är ett de facto- standarddirektiv , men det ingår inte i någon ISO C ++ -standard. Till exempel:
// Foo.h
#pragma once
class Foo
{
};
Medan #pragma once
undviker vissa problem i samband med inkluderande vakter, är ett #pragma
- per definition i standarderna - i sig en kompilator-specifik krok och ignoreras tyst av kompilatorer som inte stöder den. Projekt som använder #pragma once
är svårare att port till kompilatorer som inte stöder det.
Ett antal kodningsriktlinjer och försäkringsstandarder för C ++ avskräcker specifikt all användning av förbehandlaren annat än att #include
inkludera rubrikfiler eller för att placera inkluderande skydd i rubriker.
Villkorad logik och plattformshantering
I ett nötskal handlar villkorad förbehandlingslogik om att göra kodlogik tillgänglig eller otillgänglig för kompilering med makrodefinitioner.
Tre framstående användningsfall är:
- olika appprofiler (t.ex. felsökning, släpp, testning, optimerad) som kan vara kandidater för samma app (t.ex. med extra loggning).
- cross-platform compiles - enkel kodbas, flera kompilationsplattformar.
- att använda en gemensam kodbas för flera applikationsversioner (t.ex. Basic-, Premium- och Pro-versioner av en programvara) - med något olika funktioner.
Exempel a: En strategi över plattform för att ta bort filer (illustrativ):
#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
}
Makron som _WIN32
, __APPLE__
eller __unix__
är normalt fördefinierade av motsvarande implementeringar.
Exempel b: Aktivera ytterligare loggning för en felsökning:
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"
}
Exempel c: Aktivera en premiumfunktion i en separat produktbyggnad (Obs: detta är illustrativt. Det är ofta en bättre idé att låta en funktion låsas upp utan att behöva installera ett program igen)
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
}
Några vanliga knep:
Definiera symboler vid kallelse:
Förprocessorn kan anropas med fördefinierade symboler (med valfri initialisering). Detta kommando till exempel ( gcc -E
kör endast förprocessorn)
gcc -E -DOPTIMISE_FOR_OS_X -DTESTING_MODE=1 Sample.cpp
bearbetar Sample.cpp på samma sätt som det skulle om #define OPTIMISE_FOR_OS_X
och #define TESTING_MODE 1
lades till toppen av Sample.cpp.
Se till att ett makro definieras:
Om ett makro inte definieras och dess värde jämförs eller kontrolleras, antar förbehandlaren nästan alltid tyst att värdet är 0
. Det finns några sätt att arbeta med detta. Ett tillvägagångssätt är att anta att standardinställningarna representeras som 0, och eventuella ändringar (t.ex. i appbyggnadsprofilen) måste göras uttryckligen (t.ex. ENABLE_EXTRA_DEBUGGING = 0 som standard, ställ in -DENABLE_EXTRA_DEBUGGING = 1 för att åsidosätta). En annan metod är att definiera alla definitioner och standardvärden. Detta kan uppnås med hjälp av en kombination av #ifndef
och #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
Makron
Makron kategoriseras i två huvudgrupper: objektliknande makron och funktionsliknande makron. Makro behandlas som en symbolersättning tidigt i sammanställningsprocessen. Detta innebär att stora (eller upprepande) kodavsnitt kan abstraheras till ett förbehandlingsmakro.
// 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);
Qt-biblioteket använder denna teknik för att skapa ett meta-objekt-system genom att användaren deklarerar Q_OBJECT-makroet i spetsen för den användardefinierade klassen som förlänger QObject.
Makronamn skrivs vanligtvis i alla kepsar för att göra dem lättare att skilja från normal kod. Detta är inte ett krav, men anses bara vara bra stil av många programmerare.
När ett objektliknande makro stöter på expanderas det som en enkel kopieringspasta-operation, där makrotnamnet ersätts med dess definition. När ett funktionsliknande makro stöds utökas både namnet och parametrarna.
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))
På grund av detta är funktionsliknande makroparametrar ofta inneslutna inom parentes, som i AREA()
ovan. Detta för att förhindra eventuella buggar som kan uppstå under makroutvidgning, specifikt buggar orsakade av att en enda makroparameter består av flera faktiska värden.
#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));
Observera också att på grund av denna enkla utvidgning måste man se till att parametrarna skickas till makron för att förhindra oväntade biverkningar. Om parametern ändras under utvärderingen kommer den att ändras varje gång den används i det utvidgade makroet, vilket vanligtvis inte är vad vi vill ha. Detta gäller även om makroen stänger in parametrarna inom parentes för att förhindra att expansion bryter något.
int oops = 5;
double incremental_damage = AREA(oops++);
// Compiler sees:
double incremental_damage = (3.14159265358979*(oops++)*(oops++));
Dessutom ger makron ingen typsäkerhet, vilket leder till svåra att förstå fel om typmatchning.
Eftersom programmerare normalt avslutar linjer med en semikolon, är makroer som är avsedda att användas som fristående linjer ofta utformade för att "svälja" en semikolon; detta förhindrar att oavsiktliga buggar orsakas av en extra semikolon.
#define IF_BREAKER(Func) Func();
if (some_condition)
// Oops.
IF_BREAKER(some_func);
else
std::cout << "I am accidentally an orphan." << std::endl;
I det här exemplet bryter den oavsiktliga dubbla semikolon blocket if...else
, vilket hindrar kompilatorn från att matcha den else
till if
. För att förhindra detta utelämnas semikolonen från makrodefinitionen, vilket gör att den "sväljer" semikolonen omedelbart efter användning av den.
#define IF_FIXER(Func) Func()
if (some_condition)
IF_FIXER(some_func);
else
std::cout << "Hooray! I work again!" << std::endl;
Om du lämnar det efterföljande semikolonet gör det också möjligt att använda makroen utan att avsluta det nuvarande uttalandet, vilket kan vara fördelaktigt.
#define DO_SOMETHING(Func, Param) Func(Param, 2)
// ...
some_function(DO_SOMETHING(some_func, 3), DO_SOMETHING(some_func, 42));
Normalt slutar en makrodefinition i slutet av raden. Om ett makro behöver täcka flera rader, kan emellertid ett backstopp användas i slutet av en rad för att indikera detta. Detta motstreck måste vara det sista tecknet i raden, vilket indikerar för förbehandlaren att följande rad ska sammanlänkas på den aktuella linjen och behandla dem som en enda rad. Detta kan användas flera gånger i rad.
#define TEXT "I \
am \
many \
lines."
// ...
std::cout << TEXT << std::endl; // Output: I am many lines.
Detta är särskilt användbart i komplexa funktionsliknande makron, som kan behöva täcka flera rader.
#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'.")
I fallet med mer komplexa funktionsliknande makron kan det vara användbart att ge dem sitt eget utrymme för att förhindra eventuella namnkollisioner eller för att få objekt att förstöras i slutet av makro, liknande en faktisk funktion. Ett vanligt formspråk för detta är do medan 0 , där makroen är innesluten i ett do-while- block. Detta block följs vanligtvis inte med en semikolon, vilket tillåter det att svälja en semikolon.
#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);
Det finns också variadiska makron; på samma sätt som variadfunktioner, dessa tar ett variabelt antal argument och utökar dem alla istället för en speciell "Varargs" __VA_ARGS__
, __VA_ARGS__
.
#define VARIADIC(Param, ...) Param(__VA_ARGS__)
VARIADIC(printf, "%d", 8);
// Compiler sees:
printf("%d", 8);
Observera att under expansion kan __VA_ARGS__
placeras var som helst i definitionen och kommer att expanderas korrekt.
#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);
I fallet med en variadisk parameter med nollargument kommer olika kompilatorer att hantera efterföljande komma annorlunda. Vissa kompilatorer, som Visual Studio, kommer att tyst svälja komma utan någon speciell syntax. Andra kompilatorer, som GCC, kräver att du placerar ##
omedelbart före __VA_ARGS__
. På grund av detta är det klokt att villkorligt definiera variadiska makron när portabilitet är ett problem.
// 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 */
Felmeddelanden för förprocessorn
Kompileringsfel kan genereras med förprocessorn. Detta är användbart av ett antal skäl, av vilka några inkluderar, meddela en användare om de är på en plattform som inte stöds eller en kompilator som inte stöds.
t.ex. Returfel om gcc-versionen är 3.0.0 eller tidigare.
#if __GNUC__ < 3
#error "This code requires gcc > 3.0.0"
#endif
t.ex. Returfel vid kompilering på en Apple-dator.
#ifdef __APPLE__
#error "Apple products are not supported in this release"
#endif
Fördefinierade makron
Fördefinierade makron är de som kompilatorn definierar (i motsats till de användare som definierar i källfilen). Dessa makron får inte definieras om eller definieras av användaren.
Följande makron är fördefinierade av C ++ -standarden:
-
__LINE__
innehåller__LINE__
för den linje som detta makro används på och kan ändras genom#line
direktivet. -
__FILE__
innehåller filnamnet på filen den här makroen används i och kan ändras genom#line
direktivet. -
__DATE__
innehåller datum (i"Mmm dd yyyy"
-format) för filkompileringen, där Mmm formateras som om det erhålls genom ett samtal tillstd::asctime()
. -
__TIME__
innehåller tiden (i formatet"hh:mm:ss"
) för filkompileringen. -
__cplusplus
definieras av (överensstämmande) C ++ - kompilatorer medan man sammanställer C ++ - filer. Dess värde är standardversionen som kompilatorn är helt i överensstämmelse med, dvs199711L
för C ++ 98 och C ++ 03,201103L
för C ++ 11 och201402L
för C ++ 14-standarden.
-
__STDC_HOSTED__
definieras till1
om implementeringen är värd , eller0
om den är fristående .
-
__STDCPP_DEFAULT_NEW_ALIGNMENT__
innehåller ensize_t
bokstavlig, vilket är den inriktning som används för ett samtal till inriktning-omedvetnaoperator new
.
Dessutom tillåts följande makron att fördefinieras av implementeringar och kanske inte är närvarande:
-
__STDC__
har implementeringsberoende betydelse och definieras vanligtvis endast när man sammanställer en fil som C för att indikera fullständig C-standardöverensstämmelse. (Eller aldrig, om kompilatorn beslutar att inte stödja detta makro.)
-
__STDC_VERSION__
har implementeringsberoende betydelse, och dess värde är vanligtvis C-versionen, på samma sätt som hur__cplusplus
är C ++ -versionen. (Eller definieras inte ens om kompilatorn beslutar att inte stödja detta makro.) -
__STDC_MB_MIGHT_NEQ_WC__
definieras till1
, om värden för den smala kodningen för grundteckenuppsättningen kanske inte är lika med värdena på deras breda motsvarigheter (t.ex. om(uintmax_t)'x' != (uintmax_t)L'x'
) -
__STDC_ISO_10646__
definieras omwchar_t
är kodad som Unicode och expanderar till ett heltalskonstant i formenyyyymmL
, vilket indikerar den senaste Unicode-versionen som stöds. -
__STDCPP_STRICT_POINTER_SAFETY__
definieras till1
, om implementeringen har strikt pekarsäkerhet (annars har den avslappnad pekarsäkerheten ) -
__STDCPP_THREADS__
definieras till1
, om programmet kan ha mer än en tråd för körning (tillämpligt på fristående implementering - värd implementationer kan alltid ha mer än en tråd)
Det är också värt att nämna __func__
, som inte är ett makro, utan en fördefinierad funktionslokal variabel. Den innehåller namnet på den funktion den används i, som en statisk teckenuppsättning i ett implementeringsdefinerat format.
Ovanpå dessa standardfördefinierade makron kan kompilatorer ha sin egen uppsättning fördefinierade makron. Man måste hänvisa till kompilaterdokumentationen för att lära sig dessa. T.ex:
Några av makronen är bara för att fråga stöd för någon funktion:
#ifdef __cplusplus // if compiled by C++ compiler
extern "C"{ // C code has to be decorated
// C library header declarations here
}
#endif
Andra är mycket användbara för felsökning:
bool success = doSomething( /*some arguments*/ );
if( !success ){
std::cerr << "ERROR: doSomething() failed on line " << __LINE__ - 2
<< " in function " << __func__ << "()"
<< " in file " << __FILE__
<< std::endl;
}
Och andra för trivial versionskontroll:
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-makron
En idiomatisk teknik för att generera upprepande kodstrukturer vid sammanställningstid.
En X-makro består av två delar: listan och körningen av listan.
Exempel:
#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;
}
som utvidgas av förprocessorn till följande:
Animal dog;
Animal cat;
Animal racoon;
int main() {
dog.say();
cat.say();
racoon.say();
return 0;
}
När listor blir större (låt oss säga mer än 100 element) hjälper den här tekniken att undvika överdrivet klistra in kopior.
Källa: https://en.wikipedia.org/wiki/X_Macro
Se även: X-makron
Om du inte definierar ett seamingly irrelevant X
innan du använder LIST
, kan du också skicka ett makronamn som ett argument:
#define LIST(MACRO) \
MACRO(dog) \
MACRO(cat) \
MACRO(racoon)
Nu anger du uttryckligen vilken makro som ska användas när du utvidgar listan, t.ex.
#define FORWARD_DECLARE_ANIMAL(name) Animal name;
LIST(FORWARD_DECLARE_ANIMAL)
Om varje anrop av MACRO
bör ta ytterligare parametrar - konstant med avseende på listan, kan variadiska makron användas
//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__))
Det första argumentet tillhandahålls av LIST
, medan resten tillhandahålls av användaren i LIST
anropet. Till exempel:
#define FORWARD_DECLARE(name, type, prefix) type prefix##name;
LIST(FORWARD_DECLARE,Animal,anim_)
LIST(FORWARD_DECLARE,Object,obj_)
kommer att utvidgas till
Animal anim_dog;
Animal anim_cat;
Animal anim_racoon;
Object obj_dog;
Object obj_cat;
Object obj_racoon;
#pragma en gång
De flesta, men inte alla, C ++ #pragma once
stödjer #pragma once
direktivet en #pragma once
vilket säkerställer att filen endast ingår en gång i en enda sammanställning. Det ingår inte i någon ISO C ++ -standard. Till exempel:
// Foo.h
#pragma once
class Foo
{
};
Medan #pragma once
undviker vissa problem i samband med inkluderande vakter , är ett #pragma
- per definition i standarderna - i sig en kompileringsspecifik krok och ignoreras tyst av kompilatorer som inte stöder den. Projekt som använder #pragma once
måste modifieras för att vara standardkompatibla.
Med vissa kompilatorer - särskilt de som använder förkompilerade rubriker - kan #pragma once
resultera i en betydande påskyndning av kompilationsprocessen. På liknande sätt uppnår vissa förbehandlare snabbare kompilering genom att spåra vilka rubriker som har använt inkluderar vakter. Nettoförmånen, när både #pragma once
och inkluderar vakter är anställd, beror på implementeringen och kan antingen vara en ökning eller minskning av sammanställningstiderna.
#pragma once
kombination med inkluderande vakter var den rekommenderade layouten för sidfiler när man skrev MFC-baserade applikationer på windows och genererades av Visual Studios add class
, add dialog
, add windows
guider. Därför är det mycket vanligt att hitta dem kombinerade i C ++ Windows Applicants.
Förprocessoroperatörer
#
operator eller stringizing operator används för att konvertera en makroparameter till en strängbokstav. Det kan bara användas när makronen har argument.
// 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");
Kompilatören sammanlänker två strängar och det sista printf()
-argumentet kommer att vara en strängbokstav med ny linje i slutet.
Förprocessorn ignorerar utrymmena före eller efter makroargumentet. Så under utskriften kommer vi att få samma resultat.
PRINT( This line will be converted to string by preprocessor );
Om parametern för strängbokstäver kräver en utrymningssekvens som före en dubbelcitation () kommer den automatiskt att införas av förprocessorn.
PRINT(This "line" will be converted to "string" by preprocessor);
// Compiler sees
printf("This \"line\" will be converted to \"string\" by preprocessor""\n");
##
operatör eller Token klistra operatör används för att sammanfoga två parametrar eller symboler för en makro.
// 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);
och den slutliga utgången blir
variableY = 15