Makro a funkcja w C

101

Zawsze widziałem przykłady i przypadki, w których użycie makra jest lepsze niż użycie funkcji.

Czy ktoś mógłby mi wyjaśnić na przykładzie wady makra w porównaniu z funkcją?

Kyrol
źródło
21
Postaw pytanie na głowie. W jakiej sytuacji makro jest lepsze? Użyj prawdziwej funkcji, chyba że możesz wykazać, że makro jest lepsze.
David Heffernan,

Odpowiedzi:

114

Makra są podatne na błędy, ponieważ polegają na podstawianiu tekstu i nie sprawdzają typów. Na przykład to makro:

#define square(a) a * a

działa dobrze, gdy jest używany z liczbą całkowitą:

square(5) --> 5 * 5 --> 25

ale robi bardzo dziwne rzeczy, gdy jest używany z wyrażeniami:

square(1 + 2) --> 1 + 2 * 1 + 2 --> 1 + 2 + 2 --> 5
square(x++) --> x++ * x++ --> increments x twice

Umieszczenie nawiasów wokół argumentów pomaga, ale nie eliminuje całkowicie tych problemów.

Gdy makra zawierają wiele instrukcji, możesz mieć problemy z konstrukcjami sterowania przepływem:

#define swap(x, y) t = x; x = y; y = t;

if (x < y) swap(x, y); -->
if (x < y) t = x; x = y; y = t; --> if (x < y) { t = x; } x = y; y = t;

Zwykłą strategią rozwiązywania tego problemu jest umieszczenie instrukcji wewnątrz pętli „do {...} while (0)”.

Jeśli masz dwie struktury, które zawierają pole o tej samej nazwie, ale różnej semantyce, to samo makro może działać na obu z dziwnymi wynikami:

struct shirt 
{
    int numButtons;
};

struct webpage 
{
    int numButtons;
};

#define num_button_holes(shirt)  ((shirt).numButtons * 4)

struct webpage page;
page.numButtons = 2;
num_button_holes(page) -> 8

Wreszcie, makra mogą być trudne do debugowania, powodując dziwne błędy składniowe lub błędy w czasie wykonywania, które musisz rozwinąć, aby je zrozumieć (np. Z gcc -E), ponieważ debuggery nie mogą przechodzić przez makra, jak w tym przykładzie:

#define print(x, y)  printf(x y)  /* accidentally forgot comma */
print("foo %s", "bar") /* prints "foo %sbar" */

Funkcje wbudowane i stałe pomagają uniknąć wielu z tych problemów z makrami, ale nie zawsze mają zastosowanie. Gdy makra są celowo używane do określenia zachowania polimorficznego, uniknięcie niezamierzonego polimorfizmu może być trudne. C ++ ma wiele funkcji, takich jak szablony, które pomagają tworzyć złożone konstrukcje polimorficzne w bezpieczny sposób bez użycia makr; szczegółowe informacje można znaleźć w języku programowania C ++ Stroustrupa .

D Coetzee
źródło
43
O co chodzi z reklamą w C ++?
Pacerier
4
Zgadzam się, to jest pytanie C, nie ma potrzeby dodawania stronniczości.
ideasman42
16
C ++ jest rozszerzeniem C, które dodaje (między innymi) funkcje mające na celu rozwiązanie tego konkretnego ograniczenia C. Nie jestem fanem C ++, ale myślę, że jest to na temat.
D Coetzee
1
Makra, funkcje wbudowane i szablony są często używane w celu zwiększenia wydajności. Są nadużywane i mają tendencję do obniżania wydajności z powodu rozdęcia kodu, co zmniejsza efektywność pamięci podręcznej instrukcji procesora. Możemy tworzyć szybkie ogólne struktury danych w C bez korzystania z tych technik.
Sam Watkins
1
Zgodnie z normą ISO / IEC 9899: 1999, §6.5.1, „Pomiędzy poprzednim a następnym punktem sekwencji obiekt powinien mieć swoją przechowywaną wartość zmodyfikowaną co najwyżej raz przez ocenę wyrażenia”. (Podobne sformułowanie istnieje w poprzednich i kolejnych standardach C). Nie x++*x++można więc powiedzieć, że wyrażenie zwiększa xdwukrotnie; w rzeczywistości wywołuje niezdefiniowane zachowanie , co oznacza, że ​​kompilator może robić wszystko, co chce - może zwiększyć xdwukrotnie, raz lub wcale; może zakończyć się błędem, a nawet sprawić, że demony wylecą ci z nosa .
Psychonaut
40

Funkcje makro :

  • Makro jest wstępnie przetworzone
  • Brak sprawdzania typu
  • Zwiększa długość kodu
  • Używanie makro może prowadzić do efektów ubocznych
  • Szybkość wykonania jest szybsza
  • Nazwa makra przed kompilacją jest zastępowana wartością makra
  • Przydatne, gdy mały kod pojawia się wielokrotnie
  • Makro tak nie sprawdza błędów kompilacji

Cechy funkcji :

  • Funkcja jest kompilowana
  • Sprawdzanie typu zostało zakończone
  • Długość kodu pozostaje taka sama
  • Nie efektu ubocznego
  • Szybkość wykonania to wolniejsza
  • Podczas wywołania funkcji następuje przekazanie kontroli
  • Przydatne, gdy duży kod pojawia się wielokrotnie
  • Sprawdzanie funkcji Błędy kompilacji
zangw
źródło
2
Wymagane odniesienie „szybkość wykonania jest szybsza”. Każdy, nawet dość kompetentny kompilator z ostatniej dekady, będzie działał dobrze, jeśli uważa, że ​​zapewni to korzyści w zakresie wydajności.
Voo
1
Czy nie jest tak, że w kontekście obliczeń niskopoziomowych MCU (AVR, tj. ATMega32), makra są lepszym wyborem, ponieważ nie powiększają stosu wywołań, jak to robią wywołania funkcji?
hardyVeles
1
@hardyVeles Nie tak. Kompilatory, nawet dla AVR, mogą bardzo inteligentnie wbudować kod. Oto przykład: godbolt.org/z/Ic21iM
Edward
34

Skutki uboczne są duże. Oto typowy przypadek:

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

min(x++, y)

zostanie rozszerzony do:

(x++ < y ? x++ : y)

xzostanie zwiększona dwukrotnie w tej samej instrukcji. (i niezdefiniowane zachowanie)


Pisanie makr wieloliniowych jest również uciążliwe:

#define foo(a,b,c)  \
    a += 10;        \
    b += 10;        \
    c += 10;

Wymagają znaku \na końcu każdego wiersza.


Makra nie mogą „zwracać” niczego, chyba że uczynisz to pojedynczym wyrażeniem:

int foo(int *a, int *b){
    side_effect0();
    side_effect1();
    return a[0] + b[0];
}

Nie możesz tego zrobić w makrze, chyba że używasz wyrażenia GCC. (EDYCJA: Możesz jednak użyć operatora przecinka ... przeoczyłem to ... Ale nadal może być mniej czytelny.)


Kolejność operacji: (dzięki uprzejmości @ouah)

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

min(x & 0xFF, 42)

zostanie rozszerzony do:

(x & 0xFF < 42 ? x & 0xFF : 42)

Ale &ma niższy priorytet niż <. Więc 0xFF < 42jest oceniany jako pierwszy.

Mistyczne
źródło
5
a nie umieszczanie nawiasów z argumentami makr w definicji makra może prowadzić do problemów z pierwszeństwem: np.min(a & 0xFF, 42)
ouah
O tak. Nie widziałem twojego komentarza podczas aktualizowania postu. Chyba też o tym wspomnę.
Mysticial
14

Przykład 1:

#define SQUARE(x) ((x)*(x))

int main() {
  int x = 2;
  int y = SQUARE(x++); // Undefined behavior even though it doesn't look 
                       // like it here
  return 0;
}

natomiast:

int square(int x) {
  return x * x;
}

int main() {
  int x = 2;
  int y = square(x++); // fine
  return 0;
}

Przykład 2:

struct foo {
  int bar;
};

#define GET_BAR(f) ((f)->bar)

int main() {
  struct foo f;
  int a = GET_BAR(&f); // fine
  int b = GET_BAR(&a); // error, but the message won't make much sense unless you
                       // know what the macro does
  return 0;
}

W porównaniu do:

struct foo {
  int bar;
};

int get_bar(struct foo *f) {
  return f->bar;
}

int main() {
  struct foo f;
  int a = get_bar(&f); // fine
  int b = get_bar(&a); // error, but compiler complains about passing int* where 
                       // struct foo* should be given
  return 0;
}
Flexo
źródło
13

W razie wątpliwości użyj funkcji (lub funkcji wbudowanych).

Jednak odpowiedzi tutaj głównie wyjaśniają problemy z makrami, zamiast prostego poglądu, że makra są złe, ponieważ możliwe są głupie wypadki.
Możesz być świadomy pułapek i nauczyć się ich unikać. Następnie używaj makr tylko wtedy, gdy jest ku temu dobry powód.

Istnieją pewne wyjątkowe przypadki, w których używanie makr ma zalety, takie jak:

  • Funkcje ogólne, jak wspomniano poniżej, mogą mieć makro, którego można używać do różnych typów argumentów wejściowych.
  • Zmienna liczba argumentów można mapować do różnych funkcji, a nie przy użyciu C użytkownika va_args.
    np .: https://stackoverflow.com/a/24837037/432509 .
  • Mogą one ewentualnie zawierać lokalne informacje, takie jak ciągi Debug:
    ( __FILE__, __LINE__, __func__). sprawdzić warunki przed / po,assert awarie, a nawet potwierdzenia statyczne, aby kod nie kompilował się przy niewłaściwym użyciu (głównie przydatne do debugowania kompilacji).
  • Sprawdź argumenty wejściowe, możesz przeprowadzić testy na argumentach wejściowych, takie jak sprawdzenie ich typu, sizeof, sprawdź, czy structelementy członkowskie są obecne przed rzutowaniem
    (może być przydatne w przypadku typów polimorficznych) .
    Lub sprawdź, czy tablica spełnia jakiś warunek długości.
    patrz: https://stackoverflow.com/a/29926435/432509
  • Chociaż zauważono, że funkcje sprawdzają typy, C będzie również wymuszać wartości (na przykład int / floats). W rzadkich przypadkach może to być problematyczne. Możliwe jest napisanie makr, które są bardziej szczegółowe niż funkcja dotycząca ich argumentów wejściowych. patrz: https://stackoverflow.com/a/25988779/432509
  • Ich użycie jako opakowań funkcji, w niektórych przypadkach możesz chcieć uniknąć ich powtarzania, np. ... func(FOO, "FOO");możesz zdefiniować makro, które rozszerza ciąg za Ciebiefunc_wrapper(FOO);
  • Kiedy chcesz manipulować zmiennymi w lokalnym zakresie wywołań, przekazywanie wskaźnika do wskaźnika działa normalnie, ale w niektórych przypadkach użycie makra jest mniej kłopotliwe.
    (przypisania do wielu zmiennych, dla operacji na piksel, to przykład, że możesz preferować makro od funkcji ... choć nadal zależy to w dużym stopniu od kontekstu, ponieważ inlinefunkcje mogą być opcją) .

Trzeba przyznać, że niektóre z nich opierają się na rozszerzeniach kompilatora, które nie są standardowe C. Oznacza to, że możesz skończyć z mniej przenośnym kodem lub mieć ifdefgo w środku, więc są one wykorzystywane tylko wtedy, gdy kompilator obsługuje.


Unikanie tworzenia wielu argumentów

Należy to zauważyć, ponieważ jest to jedna z najczęstszych przyczyn błędów w makrach ( x++na przykład przekazywanie , gdy makro może zwiększać się wielokrotnie) .

możliwe jest pisanie makr, które unikają skutków ubocznych z wieloma instancjami argumentów.

C11 Generic

Jeśli chcesz mieć squaremakro, które działa z różnymi typami i obsługuje C11, możesz to zrobić ...

inline float           _square_fl(float a) { return a * a; }
inline double          _square_dbl(float a) { return a * a; }
inline int             _square_i(int a) { return a * a; }
inline unsigned int    _square_ui(unsigned int a) { return a * a; }
inline short           _square_s(short a) { return a * a; }
inline unsigned short  _square_us(unsigned short a) { return a * a; }
/* ... long, char ... etc */

#define square(a)                        \
    _Generic((a),                        \
        float:          _square_fl(a),   \
        double:         _square_dbl(a),  \
        int:            _square_i(a),    \
        unsigned int:   _square_ui(a),   \
        short:          _square_s(a),    \
        unsigned short: _square_us(a))

Wyrażenia instrukcji

To jest rozszerzenie kompilatora obsługiwane przez GCC, Clang, EKOPath i Intel C ++ (ale nie MSVC) ;

#define square(a_) __extension__ ({  \
    typeof(a_) a = (a_); \
    (a * a); })

Więc wadą makr jest to, że musisz wiedzieć, jak z nich korzystać, i że nie są one tak szeroko obsługiwane.

Jedną z korzyści jest to, że w tym przypadku możesz użyć tej samej squarefunkcji dla wielu różnych typów.

ideasman42
źródło
1
„… obsługiwane tak szeroko…” Założę się, że wyrażenie, o którym wspomniałeś, nie jest obsługiwane przez cl.exe? (Kompilator MS)
gideon,
1
@gideon, poprawnie zredagowana odpowiedź, chociaż dla każdej wymienionej funkcji nie jestem pewien, czy konieczne jest posiadanie macierzy obsługi funkcji kompilatora.
ideasman42
12

Żadne sprawdzanie typu parametrów i kodu nie jest powtarzane, co może prowadzić do rozdęcia kodu. Składnia makr może również prowadzić do dowolnej liczby dziwnych przypadków brzegowych, w których średniki lub kolejność pierwszeństwa mogą przeszkadzać. Oto link, który demonstruje pewne makro zło

Michael Dorgan
źródło
6

Jedną z wad makr jest to, że debuggery czytają kod źródłowy, który nie ma rozszerzonych makr, więc uruchomienie debuggera w makrze niekoniecznie jest przydatne. Nie trzeba dodawać, że nie można ustawić punktu przerwania wewnątrz makra, tak jak w przypadku funkcji.

jim mcnamara
źródło
Punkt przerwania to bardzo ważna umowa, dzięki za wskazanie tego.
Hans
6

Funkcje sprawdzają typ. Daje to dodatkową warstwę bezpieczeństwa.

ncmathsadist
źródło
6

Dodaję do tej odpowiedzi ...

Makra są podstawiane bezpośrednio do programu przez preprocesor (ponieważ w zasadzie są to dyrektywy preprocesora). Dlatego nieuchronnie wykorzystują więcej miejsca w pamięci niż odpowiednia funkcja. Z drugiej strony wywołanie funkcji i zwrócenie wyników wymaga więcej czasu, a tego narzutu można uniknąć, używając makr.

Również makra mają specjalne narzędzia, które mogą pomóc w przenoszeniu programów na różne platformy.

W przeciwieństwie do funkcji makra nie muszą mieć przypisanego typu danych dla swoich argumentów.

Ogólnie są użytecznym narzędziem w programowaniu. W zależności od okoliczności można używać zarówno makroinstrukcji, jak i funkcji.

Nikos
źródło
3

W powyższych odpowiedziach nie zauważyłem jednej przewagi funkcji nad makrami, która moim zdaniem jest bardzo ważna:

Funkcje mogą być przekazywane jako argumenty, makra nie mogą.

Konkretny przykład: Chcesz napisać alternatywną wersję standardowej funkcji `` strpbrk '', która będzie akceptować zamiast jawnej listy znaków do wyszukania w innym ciągu, funkcję (wskaźnik do a), która zwróci 0, dopóki znak nie zostanie okazało się, że przechodzi jakiś test (zdefiniowany przez użytkownika). Jednym z powodów, dla których możesz chcieć to zrobić, jest to, że możesz wykorzystać inne standardowe funkcje biblioteczne: zamiast podawać jawny ciąg znaków pełen znaków interpunkcyjnych, możesz zamiast tego przekazać „ispunct” w ctype.h itd. Jeśli „ispunct” został zaimplementowany tylko jako makro, to nie zadziała.

Istnieje wiele innych przykładów. Na przykład, jeśli porównanie jest dokonywane za pomocą makra, a nie funkcji, nie możesz przekazać go do 'qsort' standardowego lib.h.

Analogiczna sytuacja w Pythonie to „print” w wersji 2 w porównaniu z wersją 3 (nieprzekraczalna instrukcja vs. zadowalająca funkcja).

Sean Rostami
źródło
1
Dzięki za tę odpowiedź
Kyrol
1

Jeśli przekażesz funkcję jako argument do makra, będzie ona oceniana za każdym razem. Na przykład, jeśli wywołasz jedno z najpopularniejszych makr:

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

tak

int min = MIN(functionThatTakeLongTime(1),functionThatTakeLongTime(2));

functionThatTakeLongTime zostanie ocenione 5 razy, co może znacznie obniżyć wydajność

Saffer
źródło