Kihagyás

Preprocesszor

A preprocesszornak köszönhető a rugalmas konfiguráció-kezelés, a forrásfájlok hierarchiába rendezése include-ok segítségével, illetve a szöveg-alapú makrók hasznossága is.

Az alapvető probléma a preprocesszorral az, hogy a fordító a feldolgozott forráskódot kapja meg, és nem az eredeti forráskódot, amit a fejlesztő lát. Sok esetben a két forráskód nagy mértékben eltér. Ezek az eltérések megnehezítik a program megértését a fejlesztők számára, emellett gondot okoznak a program megértést támogató eszközöknél is, pl. debugger. Sok mindent túlzásba lehet vinni, és ezt a fejlesztők gyakran meg is teszik.

A preprocesszor nyelve más, mint a C/C++ nyelv. A preprocesszor alapvetően egy szövegmanipuláló eszköz, nem "érti" mik azok a típusok, osztályok stb, nem ismeri a C nyelv elemeit, szintaktikáját. Ez nagyon rugalmassá teszi a működését, de gondokat is okozhat, pl a makrók nem biztos hogy értelmes kifejezésekké alakulnak C/C++ nyelven. Megjegyzés: más nyelvekhez kapcsolódva is használnak a nagyvilágban preprocesszort (pl. php-hez előfeldolgozásként).

A preprocesszor három legfontosabb eszköze a fájl beszúrás (include), a makrók és a feltételes fordítás. A továbiakban ezeket röviden bemutatjuk és megnézünk néhány gyakorlati módszert, amelyek általában ezek együttes használatával működnek jól.

Az include direktíva

Segítségével tudunk fájlokat beszúrni, a megjelölt fájl tartalmát a direktíva sora helyett szúrja be.

1
2
3
#include <stdio.h>  
#include <iostream>  
#include "myheader.h"    

A preprocesszálás során a fordító/preprocesszor konfigurációja tartalmaz egy listát a standard headerekről, include esetén ezeket a könyvtárakat nézi a preprocesszor és úgy keresi meg az adott fáljt. Ha speciálisan mi szeretnénk megadni egy-egy include útvonalat a fordítás során, akkor ezt megtehetjük a -I kapcsoló használatával, amely után meg kell adnunk a kívánt útvonalat.

A következő parancs kiírja a gcc preprocesszora által használt útvonalakat az adott rendszeren:

1
gcc -E -Wp,-v -

Makrók

A makrók szöveges helyettesítést tesznek lehetővé a preprocesszálási fázisban.

1
#define identifier replacement

A makrók értéke a hívás helyére kerül:

1
2
3
#define TABLE_SIZE 100
int table1[TABLE_SIZE];
int table2[TABLE_SIZE]; 

Preprocesszálás után ezen kód a következőképp néz ki:

1
2
int table1[100];
int table2[100];

A makrók definícióját visszavonhatjuk és újra definiálhatjuk őket:

1
2
3
4
5
#define TABLE_SIZE 100
int table1[TABLE_SIZE];
#undef TABLE_SIZE
#define TABLE_SIZE 200
int table2[TABLE_SIZE];

Ekkor a preprocesszált állomány:

1
2
3
4
int table1[100];


int table2[200];

A makrók hatóköre nem egyezik meg a C/C++ hatókörökkel, az include direktívával behúzott fájlok újradefiniálhatják őket, akár sokadik include mélységben is.

Paraméteres makrók hívása esetén először a paraméterek helyettesítése történik meg és csak ezután kerül a hívás helyére az eredmény.

1
#define INCREMENT(x) x++

A makró nem tudja figyelembe venni a C/C++ típusokat, ezáltal rugalmasabb kódot írhatunk, mely több típusra is alkalmazható, lásd MULT makró. Azonban a makróknál különösen figyelni kell arra, hogy a helyettesítés szöveg alapon működik, az új környezetben nem mindig a kívánt hatást érjük el:

1
2
#define MULT(x, y) x * y
int z = MULT(3 + 2, 4 + 2); // int z = 3 + 2 * 4 + 2; (!) 

Javítva:

1
2
#define MULT(x, y) (x) * (y)
int z = MULT(3 + 2, 4 + 2); // int z = (3 + 2) * (4 + 2)

Paraméteres makrók esetén az argumentumot konkatenálhatjuk és sztringgé alakíthatjuk:

1
2
#define str(x) #x
cout << str(test); // cout << "test";
1
2
#define glue(a,b) a ## b
glue(c,out) << "test"; // cout << "test";
1
2
#define PRINT_TOKEN(token) printf(#token " is %d", token)
PRINT_TOKEN(foo) // printf("foo" " is %d" foo)

Változó paraméterlista is megadható (variadic macros):

1
2
#define eprintf(…) fprintf (stderr, __VA_ARGS__)
eprintf ("%s:%d: ", input_file, lineno)

Léteznek előre definiált makrók, melyeket a preprocesszor biztosít, és akár dinamikusan változhatnak is futás közben (pl. LINE aktuális programsor sorszámát helyettesíti be híváskor).

  1. DATE The current date as a character literal in "MMM DD YYYY" format.
  2. TIME The current time as a character literal in "HH:MM:SS" format.
  3. FILE This contains the current filename as a string literal.
  4. LINE This contains the current line number as a decimal constant.
  5. STDC Defined as 1 when the compiler complies with the ANSI standard.

Feltételes fordítás

A fordítási egység összeállításánál nem csak fájlonként, hanem soronként is szabályozni tudjuk, mit kapjon meg a fordító: #if #ifdef #ifndef #else #elif #endif. Adott esetben egyes programrészleteket így ki tudunk egyszerűen venni a forráskódból, és ezzel már a fordítónak nem is kell foglalkoznia.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

int main(void)
{
   static int array[ ] = { 1, 2, 3, 4, 5 };
   int i;

   for (i = 0; i <= 4; i++)
   {
      array[i] *= 2;

#if TEST >= 1
   printf("i = %d\n", i);
   printf("array[i] = %d\n",
   array[i]);
#endif

   }
   return(0);
}

Egyéb direktívák

A pragma direktíva implementáció függő, adatokat szolgáltat a fordítási folyamathoz. Ha a preprocesszor/fordító ismeri, akkor végrehajtja, ha nem ismeri akkor figyelmen kívül hagyja.

1
2
3
#pragma pack(8) // 8 bájtos határra igazít
#pragma warning( disable : 2312 79 ) // letilthatunk adott warningokat -> csak végső esetben használjuk! 
pragma once // lásd később

Az error direktíva hatására megáll a folyamat egy hibaüzenettel. Általában konfigurációk ellenőrzéséhez használják, pl:

1
2
3
#if !defined(__cplusplus)
    #error A fordítás csak C++ módban végezhető el!
#endif

Gyakorlati alkalmazások

Kommentelés

Ha többsoros kommentekkel együtt szeretnénk nagyobb részt ideiglenesen kikommentezni, akkor feltételes fordítással megtehető. (Csak ideiglenesen, verziókezelőbe ne kerüljön ilyen kód...)

1
2
3
4
5
6
7
8
#if 0
/* comment ...
*/

// code

/* comment */
#endif

Konfigurációk

Az adott fordítási környezet is definiál makrókat, melyeket konfigurációkhoz használhatunk. A linux kernel kódja rengeteg makrót és feltételes direktívát tartlamaz.

1
2
3
4
5
6
7
#ifdef __LINUX__
#include <sys/socket.h>
#elif _WIN32
#include <winsock.h>
#else
// other platforms
#endif

Például a GCC verzió szerint is el tudunk ágazni ezzel a frappáns megoldással:

1
2
3
4
5
/* Test for GCC > 3.2.0 */
#if __GNUC__ > 3 || \
    (__GNUC__ == 3 && (__GNUC_MINOR__ > 2 || \
                       (__GNUC_MINOR__ == 2 && \
                        __GNUC_PATCHLEVEL__ > 0))

Include védelem

Header fájlok többszörös include-ja azonnal fordítási hibához szokott vezetni, hiszen ekkor újradefiniálhatunk programelemeket, ezért eleve #ifndef - #define - #endif védelemmel látjuk el a header fájlokat:

1
2
3
4
#ifndef _FILE_NAME_H_
#define _FILE_NAME_H_
/* code */
#endif // #ifndef _FILE_NAME_H_

Egy teljes fordítási folyamat során a sokszor használt standard headerek nagyon sokszor felhasználásra kerülhetnek, ezért még a fájl megnyitási és preprocesszálási idő is számíthat, ezért több preprocesszor is speciálisan kezeli ezt a jelenséget. Ha a fájl elején szerepel a

1
#pragma once

direktíva, akkor a headert többet nem is nyitja meg.

Megjegyzés: a gcc pragma once nélkül is optimalizál, elkerülve a felesleges munkát.

Bónusz kérdés: ha egy headert egy preprocesszálási folyamat során 1000-szer is include-olunk, előfordulhat-e hogy nem mindig ugyanaz lesz a tartalma? Igen, előfordul, a standard headerekben is van erre példa, a #undef FILE_NAME_H direktíva hatására a következő include már ismételten behúzhatja az adott headert.

Mellékhatás

Tegyük fel, hogy annyira vagány kódolók vagyunk, hogy az abszolút értéket is makróval implementáljuk:

1
#define ABS(x) ((x) < 0 ? -(x) : (x))
Vajon felkészültünk-e arra, hogy a makrót így használják? Mi lesz a result változó értéke?

1
2
int x = 5;
int result = ABS(x++);

Összegzés

A preprocesszor nélkül nehéz lenne az élet, de ha eltúlozzuk a használatát akkor lesz igazán nehéz. Az utóbbi években egyre több munka jelenik meg arról, hogy lehetne minél jobban visszaszorítani a használatát. Egy-egy tömör makró használatával kisebb lehet a kód, de sokszor csak az látja át, aki az egészet kitalálta. Mivel a makrók esetén nincsenek típusok, nyelvi elemek, így könnyen félrecsúszhat. Maga Bjarne Stroustrup, a C++ nyelv megalkotója is azon dolgozik, hogy a felesleges makróhasználatot a C++11-től felfelé visszaszorítsa .