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 once
to, 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?
c++
c++11
c-preprocessor
user1849534
źródło
źródło
#pragma
nie jest makro.#pragma
.constexpr
,inline
funkcji itemplates
, aleboost.preprocessor
ichaos
pokazać, że makra mają swoje miejsce. Nie wspominając o makrach konfiguracyjnych dla kompilatorów różnic, platform itp.Odpowiedzi:
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):
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
enum
lubconst 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 zastosowaniex2 = 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_malloc
w 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++
zaktualizujemyx
dwukrotnie, co jest niezdefiniowane i prawdopodobnie doprowadzi do różnych wartości w różnych systemach, a także do różnych wartości wynikux
.źródło
if else
problemy można rozwiązać przez owijanie makro wewnątrz ciałado { ... } while(0)
. Ten zachowuje się jak można by się spodziewać w odniesieniu doif
ifor
oraz 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)
note: expanded from macro 'begin'
i pokażą, gdziebegin
jest zdefiniowane.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ń
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 max
powyż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)
albomax<double>(a, b)
(a tym samym wyraźnie określić swój zamiar).źródło
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 ++.max
amin
po nich następuje lewy nawias. Ale nie powinieneś definiować takich makr ...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)
źródło
DIV
Makro mogą być zapisane wraz z parą () wokółb
.#define DIV(a,b)
, że nie#define DIV (a,b)
, co jest bardzo różne.#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) )
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))
źródło
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.
źródło
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.
źródło
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.
źródło
Makra preprocesora nie są złe, gdy są używane do określonych celów, takich jak:
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.
źródło