kapitola 5

Preprocesor

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

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 a jejich expanze

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

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 " #par " 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

  1. Pokud se jedná o makro s argumenty, nejdříve prozkoumáme jestli předané argumenty obsahují makra. Pokud ano, jsou expandována.
  2. 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í ##.
  3. Pokud se ve výsledném textu nachází nějaká makra, jsou expandována.

Makra s parametry by se měla zvenku chovat jako funkce

Makra jsou v zásadě dvojího typu (bloková a nebloková)

Problémy: makro potrebuje sezrat stredník.

Variadická makra

Makra vs Funkce

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

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.