Przejdź do głównej zawartości

Makra i preprocesor


Jako programista klasycznego C przyszło mi wielokrotnie ścierać się z makrami. Makro to zestaw instrukcji umieszczanych w kodzie są jednak interpretowane nie przez kompilator ale przez preprocesor. Preprocesor jest „pomocnikiem” kompilatora, zajmuje się on np. wstawianiem treści plików nagłówkowych do plików z kodem za pomocą instrukcji #include. Preprocesor należy rozumieć jako prymitywny edytor tekstu dokonujący „korekcji” plików z kodem źródłowym przed rozpoczęciem ich przetwarzania przez kompilator.
Jakie są zalety wykorzystywania tych rozwiązań w kodzie? Tak naprawdę w języku C w czasach przed wprowadzeniem słowa kluczowego inline umożliwiały wstawianie kodu we wskazane miejsce. Należy bowiem pamiętać że każdorazowe wstawienie makroinstrukcji powoduje ingerencję w kod źródłowy (innymi słowy we wskazanym miejscu zostanie wstawiony stosowny fragment kodu). Łatwo obserwowalnym efektem częstego wykorzystywania makr w plikach z kodem jest rozrost pliku binarnego oraz jego szybsze działanie.
Gdzie wykorzystywane są makra? Najczęściej spotkać je można w projektach tworzonych dla systemów wbudowanych. Powodem tego jest często niedoskonałość wykorzystywanego kompilatora, rozszerzanie funkcjonalności języka C o elementy obiektowe (wczesna wersja języka objective-c - Object-Oriented Pre-Compiler).
Pisząc makra należy być świadomy ich specyfiki. W szczególności kod który w dużej części pokryty jest makrami staje się trudny w debugowaniu, co więcej błędy kompilacji mogą być sygnalizowane w zupełnie innych liniach (kompilator przetwarza plik po przejściu preprocesora który rozwija makrodefinicje do zwykłego kodu). I w końcu najważniejsze, makra traktowane jako alternatywa dla funkcji inline nie interpretują typów.
Zobaczmy zatem jaki ten diabeł straszny. Oto przykład makr:

#define MAX(a,b) (a)<(b)?(b):(a)

#define MIN(a,b) (a)<(b)?(a):(b)

#define BIT_MOV_LEFT(data, move) (data)<<(move)

#define BIT_MOV_RIGHT(data, move) (data)>>(move)

//bardzo zła implementacja potęgowania
#define FAST_POW(a,b) \
  int aa = a; \
  for(int i = 0; i < b; ++i) \
  a*=aa;

Z czterema pierwszymi każdy kto programował na urządzenia wbudowane najprawdopodobniej miał styczność. Ich wykonanie spowoduje wstawienie kodu porównania/przesunięcia w miejsce wstawienia makroinstrukcji. Jest to świetna alternatywa dla klasycznych funkcji w języku C. Co więcej wystarczy jedna definicja każdego z tych makr a porównywać i przesuwać można zmienne dowolnych typów i nie spowoduje to ostrzeżeń kompilatora (oczywiście w ramach rozsądku, próba porównania struktur w języku C operatorem mniejszości spowoduje błąd kompilacji).
Co z ostatnim makrem implementującym potęgowanie (ta implementacja jest zła na bardzo wiele sposobów a fakt że jest makrodefinicją czyni ją jeszcze gorszą – skupimy się jedynie na wadach związanych z makrami)? Podstawowym problemem z z makrem FAST_POW jest tworzenie zmiennej na potrzeby makra aa. Po wykorzystaniu makra FAST_POW zmienna aa będzie znana w w całym bloku kodu. Powoduje to iż niemożliwym jest wielokrotne użycie tego makra w danym zakresie ważności, ponadto tracimy obszar pamięci na stosie o rozmiarze zmiennej int (rozmiar zmiennej jest zależny od wykorzystywanej architektury procesora oraz wykorzystywanych optymalizacji w procesie kompilacji). Problem ten może stać się ważny dla systemów wbudowanych z mała ilością pamięci. Wiemy już zatem że wywołanie

FAST_POW(a,b);
FAST_POW(c,d);

jest niemożliwe kompilator bowiem zwróci błąd mówiący o tym że zmienna aa już istnieje. Niestety patrząc na te 2 linijki kodu nie jesteśmy wstanie stwierdzić o jakie zmienne aa chodzi, przecież nigdzie ich nie ma. Dopiero analiza definicji makra FAST_POW pozwoli nam zlokalizować miejsce problemu. Jest to powszechny problem i łatwo go rozwiązać.

#define FAST_POW(a,b) \
{ \
  int aa = a; \
  for(int i = 0; i < b; ++i) \
  a*=aa; \
}

Teraz dzięki ujęciu „treści” makra w nawiasy klamrowe wyznaczające zakres ważności zmiennych automatycznych zmienna aa znana będzie jedynie w zakresie ciała makra.
Poniżej kolejne wywołanie FAST_POW nastręczające problemów

FAST_POW(a+5, b)

Przy takim wywołaniu makra FAST_POW kompilator poinformuje nas o tym iż do lewostronnego wrażenia nie można wykonać operacji przypisania. Niezgodność ta występuje w linijce

a*=aa; // linia rozwija się do a+5*=aa;

Ostatnią rzeczą na jaka warto zwrócić uwagę jest fakt że w przypadku makr złożonych (zawierających więcej niż jedną linię/instrukcję) i wykorzystania znaku złamania linii „\” pojawia się ryzyko wystąpienia niezwykle kłopotliwego błędu. Mianowicie po znaku złamania linii nie może znajdować się żaden inny znak w szczególności nie może to być „spacja”. Jest to niezwykle trudny do wykrycia błąd gdyż bez specjalnych technik (np. wyświetlanie białych znaków w edytorze) i doświadczenia (komunikat kompilatora o błędzie w tym przypadku nam nie pomoże) nie jest łatwo zlokalizować usterkę.

Wróćmy do czterech pierwszych makrodefinicji MAX, MIN, BIT_MOV_RIGHT, BIT_MOV_LEFT. Wspomniane makrodefinicje napisane zostały zgodnie ze sztuką, lecz nawet to nie chroni programisty przed popełnieniem trudnego do znalezienia błędu. Przeanalizujmy poniższą linijkę

MAX(++a,b); //linia rozwija się do (++a)<(b)?(b):(++a)

Dla takiego wywołania nasza zmienna a może zostać zwiększona o 2 podczas gdy na wywołanie MAX(++a, b) sugeruje zwiększenie wartości tylko o 1.

Dodatkowym smaczkiem w temacie rozwinięć makr jest fakt dopisywania nawiasów dla parametrów przekazywanych do makra. Poniżej kod prezentujący makro oraz jego użycie majce podnieść wskazaną wartość do kwadratu:

#define POW_2(a) a*a
a = POW_2(a+5); //makro rozwija się do a+5*a+5

Jak łatwo zauważyć otrzymany wynik znacznie różni się od oczekiwanej wartości. Powód podany jest w komentarzu i nie wymaga dalszych wyjaśnień. Poprawna implementacja makra POW_2 wygląda następująco:

#define POW_2(a) (a)*(a)

a = POW_2(a+5); //makro rozwija się do (a+5)*(a+5)


Do tej pory zaprezentowane zostały przykłady mające na celu uwypuklić możliwe błędy i zawiłości podczas tworzenia makr. Problemy te dotyczą sytuacji gdy programista usiłuje zastąpić funkcje wraz ze słowem kluczowym inline za pomocą działań preprocesora. Zobaczmy kilka bardzo praktycznych zastosowań makr w codziennym życiu programisty :

#define CLEAN (ptr) \
  free(ptr); \
  ptr = NULL;

#define MALLOC _WITH_INIT(ptr, size) \
  ptr = malloc(size); \
  assert(!ptr); \
  memset(ptr, 0, size);

MALLOC _WITH_INIT alokuje pamięć pod dany wskaźnik o zadanym rozmiarze dodatkowo w przypadku niepowodzenia operacji przydziału pamięci programista otrzyma asercję. Przydzielona pamięć wypełniana jest 0.
Makro CLEAN jako parametr przyjmuje wskaźnik następnie zwalnia obszar pamięci wskazywany przez wskaźnik i ustawia go na NULL. Przykładów takich drobnych makr można mnożyć, makra bywają często pomocnicze, niestety ich nadużywanie prowadzi do zaciemnienia kodu.

Podstawowym zastosowaniem instrukcji preprocesora jest sterowanie procesem kompilacji.
Klasycznym przykładem niech będzie warunkowe włączenie kodu do kompilacji w zależności od systemu operacyjnego:

#ifdef __linux__
//kod zależny od platformy linux
#elif _WIN32
// kod zależny od platformy windows
#endif

Ponadto na podstawie dostarczanych flag można określić które z elementów standardu SUS (Single UNIX Specyfication)/POSIX są wspierane przez nasz system:

#ifdef SUV_POSIX_1996
  printf(„system wspiera standard: IEEE std 1003.1-1996/ISO 9945-1:1996(199506L)”);
#endif

Takie podejście jest szczególnie przydatne gdy mamy do czynienia z oprogramowaniem kompilowanym na wielu systemach z tej samej rodziny, lub gdy chcemy mieć pewność że dany zestaw funkcjonalności jest wspierany.

Bardzo ważnym wykorzystaniem makr jest tworzenia header guard'ów. Konstrukcja ta jest umieszczana w pliku nagłówkowym.

Plik fast_func.h

ifundef __FAST_FUNC__
#ddefine __FAST_FUNC__

//prototyp funkcji

#endif

Pozwala ona uniknąć wielokrotnego dołączania plików nagłówkowych w zakresie jednostki kompilacji. Pomimo iż prezentowana konstrukcja jest całkiem zgrabna, nowoczesne kompilatory pozwalają znacznie ją uprościć. Wystarczy na początku pliku *.h dodać instrukcję

#pragma once

Makra mogą być również wykorzystane do przedefiniowania typów. W systemach wbudowanych można spotkać się z zapisami podobnymi do poniższych:

#define INT short int
#define FLOAT short float

Takie przedefiniowanie pozwala zaoszczędzić nieco czasu programiście oraz w łatwy sposób w jednym miejscu podmienić definicję typów (być może w przyszłości w projekcie zajdzie potrzeba zwiększenia rozmiaru INT).
Ciekawą instrukcją często stosowaną w przypadku funkcji logujących jest __func__ (w C++11 polecam __PRETTY_FUNCTION__) zwraca string będący nazwą funkcji w której owo makro jest wywoływane. Makra __DATA__, __TIME__, __LINE__, __FILE__ są przydatne w sytuacji gdy chcemy oznaczyć ostatnią kompilację danej binarki.

Innym zastosowaniem może być podmiana (mockowanie) wywoływanych funkcji np.:

plik mock.h
#pragma once
//dla uproszczenia przykładu implementacja funkcji umieszczona w pliku *.h
#incluude <config.h> //plik zawierający flagi konfiguracyjne w tym __MOCK__
#ifdefine __MOCK__
int mock_getValueFromNet()
{
  return 5;
}


#define getValueFromNet mock_getValueFromNet
#endif

plik main.c
#include <internet.h> //nagłówek zawierający prototyp funkcji getValueFromNet pobierającej wartość z sieci
#include <mock.h>

int main()
{
  …//nieistotny fragment kodu
  int a = getValueFromNet();
  …//kod operujący na zmiennej a
  return 0;
}


Prezentowane rozwiązanie Pozwala wykorzystywać istniejący program po zrekompilowaniu pomimo braku dostępu do sieci, preprocesor podmieni ciąg znaków getValueFromNet na mock_getValueFromNet. Oznacza to że w procesie kompilacji wykorzystana zostanie nasza prywatna wersja funkcji nie wymagająca działającego połączenia sieciowego. Prezentowana technika może się przydać podczas poszukiwania wycieków pamięci (własna implementacja funkcji malloc i free). Takie podejście jest charakterystyczne dla języka C, należy jasno zaznaczyć że języki takie jak C++/objective-c/C#/Java oferują znacznie bardziej eleganckie podejście do podmiany funkcji/metod/obiektów.

Na sam koniec krótkie podsumowanie. Preprocesor wraz z dyrektywami pozwala w znaczny sposób modyfikować/rozszerzać możliwości języka C/C++. Korzystanie z makr często powoduje skrócenie zapisu pewnych oczywistych instrukcji kosztem łatwości debugowania. Nadużywanie tego typu rozwiązań znacznie utrudnia interpretację kodu. Języki wyższego poziomu takie jak C++ oferują znacznie lepsze rozwiązania (template'y, polimorfizm, dziedziczenie) i to właśnie one powinny być stosowane.

Komentarze

Popularne posty z tego bloga

C++11 Iterowanie

Zasadniczo iterowanie po elementach tablicy/vectora/listy itp. nie jest niczym nowym, interesującym ani pasjonującym, ot szara codzienność. Przyjrzyjmy się zatem jak robimy to najczęściej: class Image { public:   Image();   void rotate(float angle);   void display();   void* serialize(); }; class ShowImage { public:   void operator()(Image image)   {     image.display();   } }; //... std::vector<Image> imageCollection(10); for(int i=0; i<imageCollection.size(); ++i) {   imageCollection[i].display(); } for( std::vector<Image>::iterator                          currentImage = imageCollection.begin();                         currentImage < imageCollection.end();                         ++currentImage ) {   currentImage->display(); } std::for_each(imageCollection.begin(), imageCollection.end(),               ShowImage()); Dla naszych potrzeb stworzyliśmy sp

C++11 Variadic templates – szablony ze zmienną liczba parametrów

Witam w nowym roku. Dziś będzie o nowym elemencie szablonów czyli o szablonach ze zmienną liczbą parametrów. Ich implementacja i zachowanie różni się nieco od klasycznych szablonów. Nowa funkcjonalność pozwala na tworzenie bezpiecznych list typów. template<typename ...Ts> void variadic_template(){} Pierwszą nowością jest zastosowanie ...(wielokropka) przy określaniu typów szablonu. W ten sposób sygnalizujemy mnogość typów. Niestety nie mamy możliwości iterowania po kolejnych typach wewnątrz naszego szablonu. Ponadto taka definicja pozwala na stworzenie/wywołanie naszej szablonowej funkcji bez typów. variadic_template<>(); Aby tego uniknąć można uciec się do następującej sztuczki: template<typename T1, typename ...Ts> void variadic_template(T1 arg, Ts... args) W powyższym przykładzie jawnie wymuszamy podanie przynajmniej jednego typ dla naszej funkcji szablonowej. Jak już wspomniałem nie możemy jawnie iterować po typach przekazanych do