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

Zawsze o to pytałem, ale nigdy nie otrzymałem naprawdę dobrej odpowiedzi; myślę, że prawie każdy programista przed napisaniem pierwszego "Hello World" napotkał zdanie takie jak "makro nie powinno być używane", "makro są złe" i tak dalej, moje pytanie brzmi: dlaczego? Czy z nowym C++11 jest realna alternatywa po tylu latach?

Łatwa 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 once jest to podatne na błędy w co najmniej 2 ważnych sytuacjach: ta sama nazwa w różnych ścieżkach i z niektórymi konfiguracjami sieciowymi i systemami plików.

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

Author: a3f, 2012-12-26

7 answers

Makra są jak każde inne narzędzie - młotek użyty w morderstwie nie jest zły, ponieważ jest młotkiem. To jest złe w sposób, w jaki osoba używa go w ten sposób. Jeśli chcesz wbić w gwoździe, młotek jest idealnym narzędziem.

Jest kilka aspektów makr, które czynią je "złymi" (rozwinię je później i zaproponuję alternatywy):]}
  1. Nie można debugować makr.
  2. ekspansja makr może prowadzić do dziwnych skutków ubocznych.
  3. makra nie mają "przestrzeni nazw", więc jeśli masz makro, które koliduje z nazwą używaną w innym miejscu, otrzymujesz zamienniki makr tam, gdzie ich nie chcesz, a to zwykle prowadzi do dziwnych komunikatów o błędach.
  4. makra mogą wpływać na rzeczy, o których nie zdajesz sobie sprawy.

Więc rozwińmy trochę tutaj:

1) makr nie można debugować. Gdy masz makro, które tłumaczy się na liczbę lub ciąg znaków, 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 nie wiesz, co się dzieje.

Zamiennik : użyj enum lub const T

W przypadku makr "funkcyjnych", ponieważ debugger działa na poziomie" na linię źródłową, gdzie jesteś", twoje makro będzie działać jak pojedyncza Instrukcja, bez względu na to, czy jest to jedna instrukcja czy sto. Sprawia, że trudno jest dowiedzieć się, co się dzieje.

Replacement: Użyj funkcji-inline, jeśli musi być " szybki "( ale uważaj, że zbyt dużo inline nie jest dobre thing)

2) rozszerzenia makr mogą mieć dziwne skutki uboczne.

Słynnym jest #define SQUARE(x) ((x) * (x)) i użycie x2 = SQUARE(x++). Prowadzi to do x2 = (x++) * (x++);, który, nawet gdyby był poprawnym kodem [1], prawie na pewno nie byłby tym, czego chciał programista. Gdyby była to funkcja, byłoby dobrze zrobić x++, A x 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;

A potem

if (something) safe_divide(b, a, x);
else printf("Something is not set...");
[[15]}to staje się całkowicie zła rzecz....

Replacement : funkcje rzeczywiste.

3) makra nie mają przestrzeni nazw

Jeśli mamy makro:

#define begin() x = 0

I mamy jakiś 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;

Teraz, jaki błąd messge myślisz, że dostajesz, i gdzie szukasz błędu [zakładając, że całkowicie zapomniałeś - lub nawet nie wiesz o-makro begin, które mieszka w jakimś pliku nagłówkowym, który napisał ktoś inny? [i jeszcze więcej zabawy, jeśli włączenie tego makra przed include - toniesz w dziwnych błędach, które nie mają absolutnie żadnego sensu, gdy patrzysz na sam kod.

Replacement: cóż, nie ma tyle co zamiennik, co "reguła" - używaj tylko wielkich nazw dla makr i nigdy nie używaj wszystkich wielkich nazw dla innych rzeczy.

4) makra mają efekty, 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ć ten początek jest funkcją, która nie powinna wpływać na x.

Tego rodzaju rzeczy, i widziałem wiele bardziej złożonych przykładów, może naprawdę zepsuć twój dzień!

Replacement: albo nie używaj makra do ustawiania x, albo podaj x jako argument.

Są chwile, kiedy używanie makr jest zdecydowanie korzystne. Jednym z przykładów jest zawinięcie funkcji za pomocą makr w celu przekazania informacji o pliku / linii:

#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_malloc jako zwykłego malloca w kodzie, ale to ma dodatkowe argumenty, więc gdy dojdzie do końca i zeskanujemy "które elementy pamięci nie zostały zwolnione", możemy wydrukować miejsce przydzielenia, aby programista mógł wytropić 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 dla większości intencji i celów, to jest to, co powinniśmy rozważyć. Więc robienie x++ * x++ zaktualizuje x dwa razy, co jest niezdefiniowane i spowoduje prawdopodobnie prowadzą do różnych wartości w różnych systemach, a także do różnych wartości wyniku w x.

 122
Author: Mats Petersson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-03-21 07:51:25

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 zastępowania wyrażeń

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

Tak, dla pozycji z powyższej listy (liczby magiczne powinny być zdefiniowane za pomocą const / constexpr i wyrażenia powinny być zdefiniowane za pomocą funkcji [normal/inline/template/inline template].

Oto niektóre z problemów wprowadzonych przez definiowanie liczb magicznych jako makr i zastępowanie wyrażeń makrami (zamiast definiowania funkcji do oceny tych wyrażeń):

  • Podczas definiowania makr dla liczb magicznych kompilator nie zachowuje informacji o typie dla zdefiniowanych wartości. Może to powodować Ostrzeżenia kompilacji (i błędy) i mylić ludzi debugujących kod.

  • 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ą 6 po przypisaniu do c (tak jak byłoby, używając STD:: max zamiast makra). Zamiast tego Kod wykonuje:

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

Poza tym makra nie obsługują przestrzeni nazw, co oznacza, że definiowanie makr w kodzie ograniczy klienta kod w jakich nazwach 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żna przyjąć ich adresu:

  • Jeśli makro-jako-stała jest obliczana na liczbę magiczną, nie można przekazać jej przez adres

  • W przypadku makra jako funkcji nie można użyć go jako predykatu lub przyjmuje adres funkcji lub traktuje ją jako funktor.

Edit: jako przykład poprawna alternatywa dla #define max powyżej:

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 bycia jawnym (co faktycznie prowadzi do bezpieczniejszego, bardziej jawnego kodu): {]}

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

Jeśli ta wartość max jest zdefiniowana jako makro, kod zostanie skompilowany (z ostrzeżeniem).

Jeśli max jest zdefiniowany jako funkcja szablonu, kompilator zwróci uwagę na niejednoznaczność, a ty musisz powiedzieć albo max<int>(a, b) albo max<double>(a, b) (i tym samym wyraźnie określić swój zamiar).

 19
Author: utnapistim,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-10-10 21:20:00

Częstym problemem jest to:

#define DIV(a,b) a / b

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

Wydrukuje 10, a nie 5, ponieważ preprocesor rozszerzy go w ten sposób:

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

Ta wersja jest bezpieczniejsza:

#define DIV(a,b) (a) / (b)
 10
Author: phaazon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-03-30 08:09:06

Makra są cenne szczególnie przy tworzeniu kodu generycznego (parametry makra mogą być dowolne), czasami z parametrami.

Więcej, ten kod jest umieszczony (tj. wstawione) w punkcie makra.

OTOH, podobne wyniki można uzyskać z:

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

  • Templates, in C++ (generic parameter types and values)

  • Funkcje Inline (umieść kod, w którym są wywoływane, zamiast przeskakiwać do definicji jednopunktowej-jednak jest to raczej rekomendacja dla kompilatora).

Edit: A dlaczego makro jest złe:

1) Brak sprawdzania typu argumentów( nie mają typu), więc można je łatwo pomylić 2) czasami rozbudować do bardzo skomplikowanego kodu, który może być trudny do zidentyfikowania i zrozumienia w wstępnie przetworzonym pliku 3) łatwo jest tworzyć podatny na błędy kod w makrach, takich jak:

#define MULTIPLY(a,b) a*b

A następnie wywołaj

MULTIPLY(2+3,4+5)

Że rozszerza się w

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

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

#define MULTIPLY(a,b) ((a)*(b))
 3
Author: user1284631,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-12-26 13:49:47

Myślę, że problem polega na tym, że makra nie są dobrze zoptymalizowane przez kompilator i są "brzydkie" do odczytu i debugowania.

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

 1
Author: Davide Icardi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-12-26 13:52:02

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

Są (meta) koncepcją języka znalezioną 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ą oczywiste. Z drugiej strony mogą pomóc ci zachować czysty kod i zapisać dużo piszesz, jeśli używasz poprawnie, więc sprowadza się to do osobistych preferencji.

 1
Author: Sandi Hrvić,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-12-26 13:55:54

Makra w C / C++ mogą służyć jako ważne narzędzie do kontroli wersji. Ten sam kod może być dostarczony 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

Tego rodzaju funkcjonalność nie jest tak łatwo możliwa bez makr. Makra są w rzeczywistości doskonałym narzędziem do zarządzania konfiguracją oprogramowania, a nie tylko sposobem na tworzenie skrótów do ponownego użycia kodu. Definiowanie funkcji do celów możliwość wielokrotnego użytku w makrach może zdecydowanie stwarzać problemy.

 1
Author: indiangarg,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-07-04 19:36:15