C/C++ programozási trükkök/Trükkök makrókkal
Ebben a fejezetben a különböző makrókkal kapcsolatos trükkök kerülnek terítékre.
A makrókat a C preprocesszor értékeli ki. A C preprocesszor részletes angol nyelvű leírással rendelkezik.[1]
Elágazások tuningolása
szerkesztésA következőek C/C++ nyelven, a GCC fordító 2.96 vagy frissebb verziójával működnek. Más fordítóról nincs információ.
Sok esetben egy elágazásnál (if) az egyik eset lényegesen sűrűbben fordul elő, mint a másik. Például elég sűrűn vannak a programokban biztonsági ellenőrzések, mint például egy osztás előtt annak megvizsgálása, hogy az osztó nem nulla-e. A fordítóprogram a várható eredmény érdekében képes optimalizálni a kódot, ehhez viszont a programozónak jelezni kell az elágazás várható kimenetelét. A GCC 2.96-os verziójától kezdve erre a __builtin_expect[2] makrót lehet használni. Nézzünk először egy példát!
#ifndef LIKELY
#if __GNUC__ >= 3
#define LIKELY(x) __builtin_expect((x),1)
#define UNLIKELY(x) __builtin_expect((x),0)
#else
#define LIKELY(x) (x)
#define UNLIKELY(x) (x)
#endif
#endif
...
if (LIKELY(a>b)) {a=b;}
else {a=0;}
A fenti példa először is definiál két új makrót a LIKELY-t és az UNLIKELY-t. Ezek nem csinálnak semmit, ha 3.00-ásnál korábbi GCC-vel fordítjuk a kódot (vagy esetleg más fordítóval), de bekapcsolja a __builtin_expect-et, ha ismeri ezt a fordító. Térjünk vissza a példa elemzésére! Ha a feltétel valószínű, akkor a LIKELY arra ösztönzi a fordítót, hogy az else ág legyen kevéssé hatékonyan megvalósítva, konkrétan egy ugró utasításon keresztül, míg az igaz ág egyenesen, ugrás nélkül lesz gépi kódra fordítva. A modern processzorok elkezdik adott utasítás végrehajtása közben már feldolgozni a következő utasításokat, de a feltételes ugró utasításoknál ez nehezen megy (kétfelé kellene folytatnia az előfeldolgozást). Ehhez jön még az a trükk, hogy a processzorok az utasításcache-be beemelik a következő utasításokat. E két dolog következménye az, hogy az ugrás nélkül megvalósított ága az elágazásnak gyorsabban hajtódik végre, mint a másik ág.
Kommentezés makrókkal[3]
szerkesztésA következőek C/C++ nyelven bármilyen fordítóval működnek.
A C megjegyzéseket nem lehet rendesen egymásba ágyazni, a makrókat viszont igen, ahogy azt a következő kód szemlélteti.
#if 0
printf("Kommentezett részlet.");
#if 0
printf("Először kommentezett részlet.");
#endif
#endif
Mivel az #if fordítási időben értékelődik ki és a 0 miatt mindig hamis lesz, ezért az if/endif páros közötti kód nem kerül lefordításra. Ennek a megközelítésnek akkor van értelme, ha szeretnék 10-20 függvényt, vagy egy nagyobb osztályt átmenetileg eltávolítani a fordításból, de azok sok megjegyzéssel vannak ellátva, ezért nem könnyű egy darab nagy /* */ megjegyzésblokkba rakni őket.
Debugolás makrókkal[4]
szerkesztésA következőek C/C++ nyelven, bármilyen fordító alatt működnek.
Mottó: Nincs tökéletes program, csak olyan, aminek nem ismejük a hibáit. Egy programot célszerű jó alaposan ellátni nyomkövetéshez használatos információkkal, így hamar kiderülnek a hibák. Egy kész, kereskedelmi programnál viszont egyrészt ezek az információk lassítják a programot, csúnyává teszik a kimenetet, olyan belső információkat tartalmaznak, ami nem a felhasználóra tartozik, vagy csak valamilyen okból nem szeretnénk bent hagyni a kódban. Nézzünk példát!
#ifdef DEBUG_ON
#include <iostream>
#define DEBUG(expr) expr
#define MESSAGE(msg) std::cout<< msg
#define MESSAGELN(msg) std::cout<< msg << std::endl
#else
#define DEBUG(expr)
#define MESSAGE(msg)
#define MESSAGELN(msg)
#endif
...
DEBUG(if (a<b) abort();)
A most definiált makróknak nincs hatásuk, ha nem definiált a DEBUG_ON. Ha definiált, akkor a következőt csinálják: A MESSAGE kiír egy üzenetet a képernyőre. Ez lehet sima karakterlánc is, de bármely összetettebb objektum is C++-ban, amelynek meg van írva az alkalmas << operátora. A MESSAGELN ugyanezt csinálja, csak új sor karakterrel végződik. A DEBUG egy kódrészletet fog körbe, ami csak DEBUG módban fut le. Demonstrációként nézzük meg hogyan lehet a makrók definíciója során egyszerűbbé tenni a DEBUG-gal a kódot. Az alábbi kód pontosan ugyanazt csinálja, mint az előző, csak másként van írva:
#ifdef DEBUG_ON
#include <iostream>
#define DEBUG(expr) expr
#else
#define DEBUG(expr)
#endif
#define MESSAGE(msg) DEBUG(std::cout<< msg)
#define MESSAGELN(msg) DEBUG(std::cout<< msg << std::endl)
...
DEBUG(if (a<b) abort();)
Biztonságos memóriakezelés[5]
szerkesztésA következőek C/C++ nyelven, bármilyen fordító alatt működnek.
Sokszor kell memóriát lefoglani, s bizony a programozó időnként hibásan szabadítja fel. Vannak ennek a detektálására automatikus programok, például a w:Cppcheck, de a programozó is törekedhet minél jobb program írására.
Az alábbi példaprogram a következőt ötleten alapul: egy null pointert felszabadítani nem lehet, az hibát okoz. Azért lehet null pointer a változóban, mert vagy elfelejtettünk memóriát foglalni, vagy már egyszer felszabadítottuk. Egyik sem vészesen nagy probléma, tehát egyszerűen ilyenkor nem kell tennünk semmit, vagy legfeljebb üzenetet küldünk a programozónak, hogy majd próbálja megkeresni ennek az okát. Túlzott óvatosságnak tűnik, de a free() implementációjában semmi sem garantálja[6], hogy a pointert kinullázza felszabadítás után, ezért a biztonság kedvéért explicit beállítjuk felszabadítás után. Nézzük a példát:
#define free(pointer) if (LIKELY(pointer!=0)) {free(pointer);pointer=0;} DEBUG(else {MESSAGE("Free(?!) a null:");MESSAGELN(#ptr););}
#endif
...
free(ptr);
A példában látszik, hogy felhasználtunk pár korábbi makróit is. Nincs rájuk szükség, de egy komplex példán keresztül jobban meg lehet érteni a makrók működését.
Vannak, akik szerint az ilyen rejtett makrók veszélyesek. Ez esetben érdemes a makrót csupa nagybetűs névvel használni (FREE), s így egyértelműen látszik az átdefiniálás.