Preprocessor

In de informatica is een preprocessor een computerprogramma dat uitvoer produceert die gebruikt wordt als invoer voor andere programma's. Data laten verwerken door een preprocessor wordt preprocessen genoemd. Een preprocessor wordt vaak gebruikt om voorbereidende handelingen uit te voeren op data die vervolgens verder wordt verwerkt. De bewerkingen die een preprocessor uitvoert op de data kunnen verschillen: sommige voeren slechts eenvoudige macro's of substituties uit terwijl andere de data geheel transformeren naar de gewenste uitvoer.

Preprocessors worden vaak gebruikt als eerste bewerkingsstap op de broncode van een programmeertaal, alvorens deze te bewerken met een compiler. In sommige talen, zoals C en C++, is de preprocessor zelfs een onmisbaar onderdeel dat in vrijwel elk programma wordt gebruikt.

Lexicaal preprocessen

[bewerken | brontekst bewerken]

Een lexicale preprocessor voert operaties uit op het niveau van lexicale analyse: deze stap vindt plaats voor het parsen en een lexicale preprocessor voert vaak alleen eenvoudige substituties uit of het vervangt een rij tokens door een andere rij tokens. Deze bewerkingen kunnen door de gebruiker gespecificeerd worden, bijvoorbeeld door speciale instructies op te nemen in de data.

Preprocessen in C/C++

[bewerken | brontekst bewerken]

De meest gebruikte preprocessor is cpp, de preprocessor van C en C++. Deze kan veelvoorkomende bewerkingen uitvoeren, zoals het invoegen van broncode uit een ander bestand, het uitvoeren van macro's en conditionele compilatie. Instructies voor de preprocessor beginnen met '#'.

Deze preprocessorinstructie wordt veel gebruikt om een bestand in te voegen in het bestand waar de instructie instaat:

 #include "..." 

of

 #include <...> 

De volledige inhoud van het genoemde bestand wordt ingevoegd op de plaats waar deze instructie staat. Deze bestanden bevatten vaak (bijna altijd) definities voor bibliotheekfuncties of datatypen. Deze moeten ingevoegd worden voordat ze gebruikt kunnen worden. Een #include staat daarom doorgaans bovenaan een bestand. Deze ingevoegde bestanden worden headerbestanden genoemd aangezien ze aan het begin van het bestand ('head' in het Engels) staan. Voorbeelden hiervan zijn #include <stdio.h> en #include <math.h> die respectievelijk gebruikt worden voor invoer/uitvoer en wiskundige berekeningen.

Deze methode om code te kunnen hergebruiken is eenvoudig maar het is ook langzaam en inefficiënt aangezien er een controle noodzakelijk is om te voorkomen dat een bestand meer dan eens ingevoegd wordt. Sinds de jaren 70 zijn er betere technieken bekend om code te kunnen hergebruiken: Java en Common Lisp hebben packages, Pascal heeft units en Modula, OCaml, Haskell en Python hebben modules en D, ontworpen als een vervanging van C en C++, heeft imports.

Met #define kan een constante of macro gedefinieerd worden. Deze worden tijdens het preprocessen geplaatst op de plaats waar de waarde of functie gebruikt wordt (het inlinen van een functie/constante). Als de macro parameters heeft dan worden deze gesubstitueerd op de juiste plaats. Op deze manier kan een macro op dezelfde manier gebruikt worden als een (korte) functie. De gebruikelijke reden om dit te doen is om de extra stappen die gedaan moeten worden bij een functie-aanroep te vermijden. Bij korte functies zijn deze extra stappen (de overhead) nadelig voor de prestaties van het programma waardoor het beter is om de functie te inlinen.

Een voorbeeld:

 #define max(a,b) a>b?a:b 

Dit definieert een max macro dat de grootste van de twee parameters oplevert. Dit kan nu gebruikt worden zoals een gewone functie. Na het preprocessen verandert het volgende:

 z = max(x,y); 

dus in:

 z = x>y?x:y; 

Hoewel dit een belangrijke techniek in C is, is het ook langzaam en inefficiënt en zijn er enkele valkuilen. Als f en g functies zijn, dan kan het volgende tot onverwacht gedrag leiden:

 z = max(f(),g()); 

In deze code worden de functies niet allebei exact 1 keer geëvalueerd (om vervolgens de hoogste waarde in z te plaatse) zoals men zou kunnen denken. Echter, een van de twee functies zal twee keer worden uitgevoerd. Als die functie niet referentieel transparant is (dit wil zeggen: met zijeffecten) dan kan dit onverwachte gevolgen hebben.

Met macro's kan nieuwe syntaxis gecreëerd worden maar ook willekeurige tekst (de C compiler zal wel afdwingen dat dit geldig C is of commentaar). Een macro kan gebruikt worden alsof het een functie is maar er zijn wel verschillen. Naast het bovenstaande probleem heeft een macro geen geheugenadres waardoor een macro niet meegegeven kan worden aan een andere functie met een functiepointer.

Conditionele compilatie

[bewerken | brontekst bewerken]

Met de C preprocessor is het ook mogelijk gedeelten van de broncode onder bepaalde condities te compileren. Het is hierdoor mogelijk verschillende versies van dezelfde broncode in hetzelfde bestand te hebben. Vaak wordt conditionele compilatie gebruikt om het compileren aan te passen voor een bepaald platform of besturingssysteem, om een bepaalde versie te compileren (een debugversie bevat vaak extra code in vergelijking met de uiteindelijke versie) of om te voorkomen dat een bepaald headerbestand met #include meer dan eens wordt ingevoegd.

In veel gevallen wordt conditionele compilatie als volgt gebruikt:

 #ifndef FOO_H  #define FOO_H  #include "foo.h"  #endif 

Met behulp van een macro wordt het headerbestand foo.h alleen ingevoegd als FOO_H nog niet gedefinieerd is. De conventie is om deze macro te noemen naar het headerbestand dat ingevoegd wordt. Als de preprocessor de bovenstaande code voor het eerst tegenkomt zal de inhoud van foo.h wel gebruikt worden terwijl het bij een tweede keer overgeslagen zal worden aangezien FOO_H dan al gedefinieerd is.

Condities kunnen ook op complexere manieren gebruikt worden:

 #ifdef x  ...  #else  ...  #endif 

of

 #if x  ...  #else  ...  #endif 

Deze techniek wordt vaak gebruikt in systeemheaderbestanden om bepaalde eigenschappen te controleren waarvan de definitie afhankelijk kan zijn van het platform. De GNU C Library (glibc) gebruikt deze techniek bijvoorbeeld om ervoor te zorgen dat verschillen in hardware en het besturingssysteem correct worden geregeld met toch dezelfde interface.

Tegenwoordig wordt deze techniek niet meer gebruikt; de controles worden uitgevoerd met de gewone if ... then ... else ... en men laat het aan de compiler over om ervoor te zorgen dat er geen overbodige code in het uiteindelijke programma terechtkomt.