Preprocesor je součást překladače, která zpracovává zdrojový kód před tím, než je dále zpracován. Na zdrojovém kódu provádí textové operace (mazání, vkládání a nahrazování textu). Tyto operace jsou prováděny s následujícími cíly
- smazání komentářů,
- expanze maker,
- podmíněný překlad (podmíněné vynechání/vložení části kódu),
- vložení obsahu jiných souborů.
Překladače umožňují prohlédnout si kód poté, co je zpracován preprocesorem. Pro překladač gcc toho lze dosáhnout pomocí
gcc -E -P source.c
Je důležité si uvědomit, že preprocesor je součástí překladu. Nemůže proto například znát hodnoty proměnných, ty jsou známy až za běhu překladu. Provádí jenom textové substice.
Části kódu určené pro preprocesor, jsou uvozené znakem #. Ty, kterým preprocesor
rozumí říkáme direktivy preprocesoru.
Makra
Makra definujeme pomocí #define. Lze také oddefinovat pomocí
#undef macro_name.
Makra bez argumentů — definice symbolů
Makro definujeme následovně
#define name replacement
Preprocesor v kódu nahradí text name textem replacement. Nenahrazuje
výskyty name, které jsou součástí identifikátorů, klíčových slov nebo
se vyskytují v řetězcových literálech.
Například kód
#define PI 3.14
#define ERROR printf("Chyba")
double circle_area(double r)
{
if (r > 0) { return 2 * PI * r; }
ERROR;
return -1;
}
předělá preprocesor na následující.
double circle_area(double r)
{
if (r > 0) { return 2 * 3.14 * r; }
printf("Chyba");
return -1;
}
Symbol lze definovat i bez replacement části. Říkáme tím pouze,
že symbol je definován (o použití více později).
V případě, že replacement chceme napsat na více řádků, lze řádek zlomit
pomocí znaku \ (zpětné lomítko), přitom je to bráno jako jeden řádek.
#define HELLO printf("%s %s", \
"hello", \
"world")
Podle standardu existují některá předdefinovaná makra, například
__LINE__, expanduje na číslo řádku, na kterém se makro nachází (celočíselný literál)__FILE__, expanduje na jméno souboru (řetězcový literál)__DATE__, expanduje na aktuální datum (řetězcový literál)__TIME__, expanduje na aktuální čas (řetězcový literál)
Další makra může definovat konkrétní překladač. Například seznam maker definovaných
překladačem gcc lze vytisknout pomocí následujího řádku pro shell.
echo "" | gcc -dM -E -
Makra s argumenty
#define name(parameter-list) replacement
parameter-list je čárkami oddělený seznam jmen parametrů.
Při expanzi jsou jména parametrů vyskytující se v replacement (s jednou
výjimkou) nahrazena argumenty makra.
// makro pro druhou mocninu
#define square(x) x*x
// "volani" makra
square(value)
// vede k expanzi na
value*value
Při psaní maker je nutno dávat si pozor na mnohá nebezpečí,
například následující použití makra square vede k nečekanému
výsledku.
square(x+1)
// expanduje na
x+1*x+1
// a to je něco jiného, než očekáváme: hodnota výrazu je 2x + 1, nikoliv (x+1)*(x+1)
Při psaní maker je tak výhodné využívat uzávorkování v maximální míře: závorky okolo jmen argumentů a závorky okolo celého výrazu.
#define square(x) ((x)*(x))
Problémem může být také situace, kdy je parametr makra použit v jeho těle vícekrát, a příslušný argument je výraz s vedlejším efektem.
#define square(x) ((x)*(x))
int foo(int x)
{
printf("launching %i misiles\n", x);
return x;
}
// výraz
int y = square(foo(3));
// expanduje na
int y = ((foo(3))*(foo(3)));
// a vedlejší efekt tak nastane dvakrát
Řešením je psát makra tak, aby se v jejich těle parametry neopakovaly, nebo jim nepředávat jako argumenty výrazy s vedlejším efektem.
int z = foo(3); // vedlejší efekt nastane pouze jednou
int y = square(z);
Výraz #par v těle makra, kde par je jméno parametru,
expanduje na řetězcový literál, jehož obsahem je příslušný argument.
#define print_value(val) printf("value of " #val " is %f", val)
int main()
{
double x = 3;
double y = 4;
print_value(x + y);
return 0;
}
// expanduje na
int main()
{
double x = 3;
double y = 4;
printf("value of x + y is %f", x + y);
return 0;
}
Výraz a ## b expanduje na ab. Toto lze využít k vytváření nových
identifikátorů spojováním slov.
#define VAR(x) variable_ ## x
VAR(10) // expanduje na variable_10
VAR(f) // expanduje na variable_f
S výhodou to lze použít pro automatické vytváření specializovaných verzí generických funkcí (viz kapitola 3)
// funkce prohodí n bajtů na adresách a,b
void mem_swap(void *a, void *b, size_t n);
//specializacni makro
#define define_swap(type) void swap_ ## type (type *a, type *b) {\
mem_swap(a, b, sizeof(type)); \
}
define_swap(double)
// expanduje na
void swap_double(double *a, double *b) {
mem_swap(a, b, sizeof(double));
}
Krátké shrnutí procesu expanze maker
- Pokud se jedná o makro s argumenty, nejdříve prozkoumáme jestli předané argumenty obsahují makra. Pokud ano, jsou expandována.
- Do textu je vložena textová náhrada makra z jeho definice. U makra s
argumenty jsou jména parametrů nahrazena expanzemi argumentů,
a jsou provedeny převody na řetězcové literály a spojení pomocí
##. - Pokud se ve výsledném textu nachází nějaká makra, jsou expandována.
Variadická makra
Makra s proměnným počtem argumentů. Například
#define pprint(fmt, ...) printf("[pretty] " fmt, __VA_ARGS__)
Nepovinné argumenty v těle nahrazují __VA_ARGS. Například
pprint("%i %i", i, j);
expanduje na
printf("[pretty] " "%i %i", i, j);
Podmíněný překlad
#if constant_expression
// code
#endif
constant_expression je výraz obsahující literály, logické operátory a operátory porovnání,
operátor defined a případně již definovaná makra, která se na ně expandují.
Pokud je hodnota constant_expression nenulová, je část mezi #if a #endif v
kódu ponechána, jinak je vypuštěna. Výraz lze doplnit o #else a #elif s obvyklým významem.
Lze použít se speciálním výrazem defined(symbol), který je nenulový, pokud je
symbol již definované makro. Toto lze využít k zařazení či vynechání kódu určeného
pro specifické situace (debug/release build atd.) nebo platformy. Definovat jednoduchá makra lze totiž
i přímo pomocí přepínačů překladače, například pro gcc
// definuje makro Symbol
gcc -dSymbol
// definuje makro Symbol na literál 10
gcc -DSymbol=10
Příklad použití
#if defined(WIN)
// kód specifický pro windows
#elif defined(UNIX)
// kód specifický pro unix
#else
// kód pro ostatní platformy, případně vyvolání chyby
// viz níže
#endif
Výše používáme operátor defined, který je pravdivý, pokud je jeho argument
již definované makro.
// #if defined(SYMB) lze nahradit #ifdef SYMB
// #if !defined(SYMB) lze nahradit #ifndef SYMB
S podmíněným překladem můžeme výhodně použít dalších direktiv preprocesoru
#error messagepři překladu se vytisknemessagea je vyvolána chyba překladu#warning messagepři překladu je vypsánomessagejako warning
Například v části #else předchozího příkladu bychom mohli dát
#error Won't compile on unsupported platforms. We support only windows and unix.
Vložení obsahu jiných souborů
Direktiva #include slouží k vložení obsahu jiného souboru do zdrojového
kódu. Soubor, který se má vložit, lze specifikovat dvojím způsobem
#include <path-to-file>
#include "path-to-file"
V prvním případě je soubor hledán v předdefinovaných adresářích
(ty jsou dány nastavením systému, případně je lze předat jako argumenty
překladači) a path-to-file je brána jako relativní cesta z nějakého
takového adresáře. Druhém případě se nejdříve prohledává
adresář, ve kterém je uložen aktuální soubor (obsahující #include).
(Někdy se poté vyhledávají předdefinované adresáře.
Obvykle je nutno zabránit tomu, aby byl obsah nějakého souboru vložen
vícekrát. (To hrozí například pokud vkládáme dva různé soubory, které
samy obsahují directivu #include, která vkládá stejný soubor.) K
tomu lze výhodně využít podmíněného překladu (předpokládejme
například, že jde o soubor stdio.h)
#if !defined(__STDIO_H__)
#define __STDIO_H__
// kód
#endif
U většiny překladačů také funguje vložení následující direktivy.
#pragma once
Pragma
Mimo výše zmiňované direktivy existují i další. Například direktiva #pragma,
jejíž syntax i sémantika je závislá na konkrétním překladači.
Úkoly
-
Napište makro
is_digit(base, character)pro testování, zda je znak určený výrazem
characterčíslicí soustavy se základem danýmbase. Makro musí korektně fungovat pro základy soustav od 2 do 36 a libovolné znaky. Pro číslice s hodnotou vyšší než 9 používejte pro jednoduchost pouze velká písmena anglické abecedy.Příklady použití
if (is_digit(8,'8') != 0 { printf("Ano\n"); } else { printf("Ne\n"); } if (is_digit(10+6, '0'+4) != 0) { printf("Ano\n"); } else { printf("Ne\n"); } if (is_digit(30, '@') != 0) { printf("Ano\n"); } else { printf("Ne\n"); } // // vede k výpisu následujícího // Ne Ano Ne -
Upravte makro
#define LOG(fmt, ...) printf("[LOG] " fmt "\n", __VA_ARGS__)tak, aby tisklo i soubor a řádek, na kterém je použito.
-
K úkolu 7 z kapitoly Funkce doprogramujte makro
EMIT_SORT(type), které expanduje na definici třídící funkce protype. Přitom předpokládejte, že porovnáváci funkce protypese jmenujecompare_type(např. prodoubleje tocompare_double)