Dlaczego makra preprocesora są złe i jakie są alternatywy?


94

Zawsze o to pytałem, ale nigdy nie otrzymałem naprawdę dobrej odpowiedzi; Myślę, że prawie każdy programista jeszcze przed napisaniem pierwszego „Hello World” zetknął się z wyrażeniem „makro nie powinno być nigdy używane”, „makro jest złe” i tak dalej, moje pytanie brzmi: dlaczego? Czy po tylu latach istnieje prawdziwa alternatywa dla nowego C ++ 11?

Najłatwiejsza część dotyczy makr, takich jak #pragma, które są specyficzne dla platformy i kompilatora, a przez większość czasu mają poważne wady, takie jak #pragma onceto, które są podatne na błędy w co najmniej 2 ważnych sytuacjach: ta sama nazwa w różnych ścieżkach oraz z niektórymi konfiguracjami sieci i systemami plików.

Ale ogólnie, co z makrami i alternatywami dla ich użycia?


19
#pragmanie jest makro.
FooF

1
Dyrektywa preprocesora @foof?
user1849534

6
@ user1849534: Tak, o to chodzi ... a o radach dotyczących makr nie ma mowy #pragma.
Ben Voigt,

1
Można zrobić wiele z constexpr, inlinefunkcji i templates, ale boost.preprocessori chaospokazać, że makra mają swoje miejsce. Nie wspominając o makrach konfiguracyjnych dla kompilatorów różnic, platform itp.
Brandon

Odpowiedzi:


165

Makra są jak każde inne narzędzie - młotek użyty do morderstwa nie jest zły, ponieważ jest młotkiem. Sposób, w jaki człowiek używa go w ten sposób, jest zły. Jeśli chcesz wbijać gwoździe, młotek jest idealnym narzędziem.

Jest kilka aspektów, które sprawiają, że makra są „złe” (omówię każdy z nich później i zasugeruję alternatywy):

  1. Nie możesz debugować makr.
  2. Ekspansja makro może prowadzić do dziwnych efektów ubocznych.
  3. Makra nie mają „przestrzeni nazw”, więc jeśli masz makro, które koliduje z nazwą używaną w innym miejscu, możesz zastąpić makra tam, gdzie tego nie chcesz, co zwykle prowadzi do dziwnych komunikatów o błędach.
  4. Makra mogą wpływać na rzeczy, o których nie zdajesz sobie sprawy.

Rozwińmy więc trochę tutaj:

1) Makra nie mogą być debugowane. Kiedy masz makro, które tłumaczy się na liczbę lub ciąg, kod źródłowy będzie miał nazwę makra, a wiele debuggerów, nie możesz „zobaczyć”, na co to makro tłumaczy. Więc tak naprawdę nie wiesz, co się dzieje.

Zamiennik : użyj enumlubconst T

W przypadku makr „podobnych do funkcji”, ponieważ debugger działa na poziomie „na linię źródłową, na której się znajdujesz”, Twoje makro będzie zachowywać się jak pojedyncza instrukcja, bez względu na to, czy będzie to jedna instrukcja, czy sto. Utrudnia ustalenie, co się dzieje.

Zamiana : użyj funkcji - wbudowanych, jeśli ma być „szybka” (ale uważaj, że zbyt dużo wbudowanych nie jest dobrą rzeczą)

2) Rozszerzenia makro mogą mieć dziwne skutki uboczne.

Słynny jest #define SQUARE(x) ((x) * (x))i zastosowanie x2 = SQUARE(x++). To prowadzi dox2 = (x++) * (x++); czego, nawet gdyby był to prawidłowy kod [1], prawie na pewno nie byłby tym, czego chciał programista. Gdyby to była funkcja, byłoby dobrze zrobić x ++, a x zwiększyłby się tylko raz.

Innym przykładem jest „if else” w makrach, powiedzmy, że mamy to:

#define safe_divide(res, x, y)   if (y != 0) res = x/y;

i wtedy

if (something) safe_divide(b, a, x);
else printf("Something is not set...");

Właściwie staje się to zupełnie niewłaściwą rzeczą ...

Zastąpienie : prawdziwe funkcje.

3) Makra nie mają przestrzeni nazw

Jeśli mamy makro:

#define begin() x = 0

i mamy kod w C ++, który używa begin:

std::vector<int> v;

... stuff is loaded into v ... 

for (std::vector<int>::iterator it = myvector.begin() ; it != myvector.end(); ++it)
   std::cout << ' ' << *it;

Jaki komunikat o błędzie myślisz, że otrzymujesz i gdzie szukasz błędu [zakładając, że całkowicie zapomniałeś - lub nawet o nim nie wiedziałeś - makro begin, które znajduje się w jakimś pliku nagłówkowym, który napisał ktoś inny? [a nawet fajniejsze, gdybyś dołączył to makro przed włączeniem - utonąłbyś w dziwnych błędach, które nie mają absolutnie żadnego sensu, gdy spojrzysz na sam kod.

Zastąpienie : Cóż, nie jest to zamiennik, ale „reguła” - używaj tylko nazw wielkich liter dla makr i nigdy nie używaj nazw wielkich liter do innych rzeczy.

4) Makra mają efekty, o których nie zdajesz sobie sprawy

Weź tę funkcję:

#define begin() x = 0
#define end() x = 17
... a few thousand lines of stuff here ... 
void dostuff()
{
    int x = 7;

    begin();

    ... more code using x ... 

    printf("x=%d\n", x);

    end();

}

Teraz, bez patrzenia na makro, można by pomyśleć, że begin to funkcja, która nie powinna wpływać na x.

Takie rzeczy, a widziałem o wiele bardziej złożone przykłady, mogą NAPRAWDĘ zepsuć twój dzień!

Zamiana : albo nie używaj makra do ustawienia x, albo przekaż x jako argument.

Są chwile, kiedy używanie makr jest zdecydowanie korzystne. Jednym z przykładów jest zawijanie funkcji makrami w celu przekazania informacji o pliku / wierszu:

#define malloc(x) my_debug_malloc(x, __FILE__, __LINE__)
#define free(x)  my_debug_free(x, __FILE__, __LINE__)

Teraz możemy użyć my_debug_mallocw kodzie zwykłego malloc, ale ma on dodatkowe argumenty, więc kiedy dojdzie do końca i zeskanujemy „które elementy pamięci nie zostały zwolnione”, możemy wydrukować, gdzie dokonano alokacji, więc programista może wyśledzić wyciek.

[1] Niezdefiniowanym zachowaniem jest aktualizowanie jednej zmiennej więcej niż raz „w punkcie sekwencji”. Punkt sekwencji nie jest dokładnie tym samym, co stwierdzenie, ale w większości intencji i celów właśnie za to powinniśmy go uważać. W ten sposób x++ * x++zaktualizujemy xdwukrotnie, co jest niezdefiniowane i prawdopodobnie doprowadzi do różnych wartości w różnych systemach, a także do różnych wartości wyniku x.


6
Te if elseproblemy można rozwiązać przez owijanie makro wewnątrz ciała do { ... } while(0). Ten zachowuje się jak można by się spodziewać w odniesieniu do ifi fororaz innych potencjalnie ryzykownych sprawach kontrola przepływu. Ale tak, prawdziwa funkcja jest zwykle lepszym rozwiązaniem. #define macro(arg1) do { int x = func(arg1); func2(x0); } while(0)
Aaron McDaid

11
@AaronMcDaid: Tak, istnieje kilka obejść, które rozwiązują niektóre problemy ujawnione w tych makrach. Celem mojego postu nie było pokazanie, jak dobrze wykonywać makra, ale „jak łatwo jest źle uzyskać makra”, gdzie jest dobra alternatywa. To powiedziawszy, są rzeczy, które makra rozwiązują bardzo łatwo, i są chwile, kiedy makra też są właściwe.
Mats Petersson,

1
W punkcie 3 błędy nie stanowią już problemu. Nowoczesne kompilatory, takie jak Clang, powiedzą coś podobnego note: expanded from macro 'begin'i pokażą, gdzie beginjest zdefiniowane.
kirbyfan64sos

5
Makra są trudne do przetłumaczenia na inne języki.
Marco van de Voort

1
@FrancescoDondi: stackoverflow.com/questions/4176328/… (dość słabo w tej odpowiedzi, mówi o i ++ * i ++ i tak dalej.
Mats Petersson

21

Powiedzenie „makra są złe” zwykle odnosi się do użycia #define, a nie #pragma.

W szczególności wyrażenie odnosi się do tych dwóch przypadków:

  • definiowanie liczb magicznych jako makr

  • używanie makr do zamiany wyrażeń

z nowym C ++ 11 jest prawdziwa alternatywa po tylu latach?

Tak, dla pozycji z powyższej listy (liczby magiczne należy definiować za pomocą funkcji const / constexpr, a wyrażenia należy definiować za pomocą funkcji [normal / inline / template / inline template].

Oto niektóre z problemów wprowadzonych przez zdefiniowanie liczb magicznych jako makr i zamianę wyrażeń indeksu na makra (zamiast definiowania funkcji obliczających te wyrażenia):

  • podczas definiowania makr dla liczb magicznych kompilator nie zachowuje żadnych informacji o typie dla zdefiniowanych wartości. Może to powodować ostrzeżenia (i błędy) kompilacji i dezorientować ludzi podczas debugowania kodu.

  • definiując makra zamiast funkcji, programiści używający tego kodu oczekują, że będą działać jak funkcje, a tak nie jest.

Rozważ ten kod:

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

int a = 5;
int b = 4;

int c = max(++a, b);

Spodziewasz się, że a i c będą równe 6 po przypisaniu do c (tak jakby to było, używając std :: max zamiast makra). Zamiast tego kod wykonuje:

int c = ( ((++a) ? (b)) ? (++a) : (b) ); // after this, c = a = 7

Ponadto makra nie obsługują przestrzeni nazw, co oznacza, że ​​zdefiniowanie makr w kodzie ograniczy kod klienta w nazwach, których mogą używać.

Oznacza to, że jeśli zdefiniujesz makro powyżej (dla max), nie będziesz już mógł #include <algorithm>w żadnym z poniższych kodów, chyba że wyraźnie napiszesz:

#ifdef max
#undef max
#endif
#include <algorithm>

Posiadanie makr zamiast zmiennych / funkcji oznacza również, że nie możesz wziąć ich adresu:

  • jeśli makro-jako-stała oblicza magiczną liczbę, nie możesz jej przekazać przez adres

  • w przypadku makra jako funkcji nie można jej używać jako predykatu, pobierać adresu funkcji ani traktować jej jako funktora.

Edycja: Jako przykład poprawna alternatywa dla #define maxpowyższego:

template<typename T>
inline T max(const T& a, const T& b)
{
    return a > b ? a : b;
}

Robi to wszystko, co robi makro, z jednym ograniczeniem: jeśli typy argumentów są różne, wersja szablonu zmusza Cię do wyrażenia się wprost (co w rzeczywistości prowadzi do bezpieczniejszego, bardziej jednoznacznego kodu):

int a = 0;
double b = 1.;
max(a, b);

Jeśli to maksimum jest zdefiniowane jako makro, kod zostanie skompilowany (z ostrzeżeniem).

Jeśli to maksimum jest zdefiniowane jako funkcja szablonowa, kompilator wskaże niejednoznaczność i musisz powiedzieć albo max<int>(a, b)albo max<double>(a, b)(a tym samym wyraźnie określić swój zamiar).


1
Nie musi być specyficzne dla C ++ 11; możesz po prostu użyć funkcji, aby zastąpić użycie makr jako wyrażeń i [statyczne] const / constexpr, aby zastąpić użycie makr jako stałych.
utnapistim

1
Nawet C99 pozwala na użycie const int someconstant = 437;i może być używany prawie w każdy sposób, w jaki byłoby używane makro. Podobnie w przypadku małych funkcji. Jest kilka rzeczy, w których możesz napisać coś jako makro, które nie będzie działać w wyrażeniu regularnym w C (możesz zrobić coś, co uśrednia tablicę dowolnego typu, czego C nie może zrobić - ale C ++ ma szablony za to). Podczas gdy C ++ 11 dodaje kilka innych rzeczy, które „nie potrzebujesz do tego makr”, większość z nich została już rozwiązana we wcześniejszych wersjach C / C ++.
Mats Petersson

Wykonywanie preinkrementacji podczas przekazywania argumentu jest okropną praktyką kodowania. A każdy, kto koduje w C / C ++, nie powinien zakładać, że wywołanie funkcji nie jest makrem.
StephenG

Wiele implementacji dobrowolnie umieszcza identyfikatory w nawiasach, maxa minpo nich następuje lewy nawias. Ale nie powinieneś definiować takich makr ...
LF,

14

Typowy problem to:

#define DIV(a,b) a / b

printf("25 / (3+2) = %d", DIV(25,3+2));

Wypisze 10, a nie 5, ponieważ preprocesor rozwinie go w ten sposób:

printf("25 / (3+2) = %d", 25 / 3 + 2);

Ta wersja jest bezpieczniejsza:

#define DIV(a,b) (a) / (b)

2
ciekawy przykład, w zasadzie są to tylko tokeny bez semantyki
user1849534

Tak. Są rozwijane w sposób, w jaki są przekazywane do makra. DIVMakro mogą być zapisane wraz z parą () wokół b.
phaazon

2
Masz na myśli #define DIV(a,b), że nie #define DIV (a,b), co jest bardzo różne.
rici

6
#define DIV(a,b) (a) / (b)nie jest wystarczająco dobry; ogólnie rzecz biorąc, zawsze dodawaj skrajne nawiasy, w ten sposób:#define DIV(a,b) ( (a) / (b) )
PJTraill

3

Makra są cenne zwłaszcza przy tworzeniu kodu ogólnego (parametry makra mogą być dowolne), czasem z parametrami.

Co więcej, ten kod jest umieszczany (tj. Wstawiany) w miejscu, w którym makro jest używane.

OTOH, podobne wyniki można osiągnąć stosując:

  • przeciążone funkcje (różne typy parametrów)

  • szablony, w C ++ (ogólne typy parametrów i wartości)

  • funkcje inline (umieszczaj kod tam, gdzie są wywoływane, zamiast przeskakiwać do definicji jednopunktowej - jest to jednak raczej zalecenie dla kompilatora).

edycja: co do tego, dlaczego makro jest złe:

1) brak sprawdzania typu argumentów (nie mają typu), więc mogą być łatwo nadużywane 2) czasami rozszerzają się na bardzo złożony kod, który może być trudny do zidentyfikowania i zrozumienia w wstępnie przetworzonym pliku 3) łatwo jest popełnić błąd -prone kod w makrach, takich jak:

#define MULTIPLY(a,b) a*b

a następnie zadzwoń

MULTIPLY(2+3,4+5)

który rozszerza się w

2 + 3 * 4 + 5 (i nie do: (2 + 3) * (4 + 5)).

Aby mieć to drugie, należy zdefiniować:

#define MULTIPLY(a,b) ((a)*(b))

3

Nie sądzę, żeby było coś złego w używaniu definicji preprocesorów lub makr, jak je nazywasz.

Są to (meta) pojęcie języka, które można znaleźć w c / c ++ i jak każde inne narzędzie mogą ułatwić Ci życie, jeśli wiesz, co robisz. Problem z makrami polega na tym, że są one przetwarzane przed kodem C / C ++ i generują nowy kod, który może być wadliwy i powodować błędy kompilatora, które są prawie oczywiste. Z drugiej strony, mogą pomóc w utrzymaniu kodu w czystości i zaoszczędzić wiele pisania, jeśli są właściwie używane, więc sprowadza się to do osobistych preferencji.


Ponadto, jak wskazano w innych odpowiedziach, źle zaprojektowane definicje preprocesorów mogą tworzyć kod o prawidłowej składni, ale innym znaczeniu semantycznym, co oznacza, że ​​kompilator nie będzie narzekał, a Ty wprowadziłeś błąd w swoim kodzie, który będzie jeszcze trudniejszy do znalezienia.
Sandi Hrvić

3

Makra w C / C ++ mogą służyć jako ważne narzędzie do kontroli wersji. Ten sam kod można dostarczyć do dwóch klientów z niewielką konfiguracją makr. Używam takich rzeczy jak

#define IBM_AS_CLIENT
#ifdef IBM_AS_CLIENT 
  #define SOME_VALUE1 X
  #define SOME_VALUE2 Y
#else
  #define SOME_VALUE1 P
  #define SOME_VALUE2 Q
#endif

Taka funkcjonalność nie jest tak łatwa do zrealizowania bez makr. Makra są w rzeczywistości doskonałym narzędziem do zarządzania konfiguracją oprogramowania, a nie tylko sposobem tworzenia skrótów do ponownego wykorzystania kodu. Definiowanie funkcji w celu ich ponownego wykorzystania w makrach z pewnością może powodować problemy.


Ustawienie wartości Macro w linii cmd podczas kompilacji, aby zbudować dwa warianty z jednej bazy kodu, jest naprawdę fajne. z umiarem.
kevinf

1
Z pewnej perspektywy to użycie jest najbardziej niebezpieczne: narzędzia (IDE, analizatory statyczne, refaktoryzacja) będą miały trudności z ustaleniem możliwych ścieżek kodu.
erenon

1

Myślę, że problem polega na tym, że kompilator nie optymalizuje makr, a ich odczytywanie i debugowanie jest „brzydkie”.

Często dobrą alternatywą są funkcje ogólne i / lub funkcje wbudowane.


2
Co prowadzi do przekonania, że ​​makra nie są dobrze zoptymalizowane? Są prostym zastępowaniem tekstu, a wynik jest optymalizowany tak samo, jak kod napisany bez makr.
Ben Voigt,

@BenVoigt, ale nie biorą pod uwagę semantyki, a to może prowadzić do czegoś, co można uznać za „nieoptymalne” ... przynajmniej to moja pierwsza myśl o tym stackoverflow.com/a/14041502/1849534
user1849534

1
@ user1849534: Nie to oznacza słowo „zoptymalizowany” w kontekście kompilacji.
Ben Voigt,

1
@BenVoigt Dokładnie, makra to po prostu podstawianie tekstu. Kompilator po prostu powiela kod, nie jest to problem z wydajnością, ale może zwiększyć rozmiar programu. Jest to szczególnie ważne w niektórych kontekstach, w których istnieją ograniczenia rozmiaru programu. Część kodu jest tak pełna makr, że rozmiar programu jest podwójny.
Davide Icardi

1

Makra preprocesora nie są złe, gdy są używane do określonych celów, takich jak:

  • Tworzenie różnych wersji tego samego oprogramowania przy użyciu konstrukcji typu #ifdef, na przykład wydawanie okien dla różnych regionów.
  • Do definiowania wartości związanych z testowaniem kodu.

Alternatywy - do podobnych celów można użyć jakiegoś rodzaju plików konfiguracyjnych w formacie ini, xml, json. Jednak ich użycie będzie miało wpływ na kod w czasie wykonywania, którego może uniknąć makro preprocesora.


1
ponieważ C ++ 17 constexpr if + plik nagłówkowy, który zawiera zmienne constexpr „config”, może zastąpić # ifdef's.
Enhex
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.