Czy lepiej jest stosować dyrektywę preprocesora, czy jeśli (stała) instrukcja?

10

Załóżmy, że mamy bazę kodów, która jest używana dla wielu różnych klientów, i mamy w niej trochę kodu, który jest odpowiedni tylko dla klientów typu X. Czy lepiej jest użyć dyrektyw preprocesora, aby uwzględnić ten kod tylko u klienta typu X lub używać instrukcji if? Aby być bardziej zrozumiałym:

// some code
#if TYPE_X_COSTUMER  = 1
// do some things
#endif
// rest of the code

lub

if(TYPE_X_COSTUMER) {
    // do some things
}

Argumenty, o których mogę pomyśleć, to:

  • Dyrektywa preprocesora skutkuje mniejszą powierzchnią kodu i mniejszą liczbą gałęzi (w przypadku nieoptymalizujących kompilatorów)
  • Jeśli wyniki zawierają kod, który zawsze się kompiluje, np. Jeśli ktoś popełni błąd, który zaszkodzi nieistotnemu kodowi dla projektu, nad którym pracuje, błąd nadal będzie się pojawiał i nie uszkodzi bazy kodu. W przeciwnym razie nie będzie świadomy zepsucia.
  • Zawsze mówiono mi, że wolę używać procesora niż preprocesora (jeśli jest to w ogóle argument ...)

Co jest lepsze - gdy mówimy o bazie kodu dla wielu różnych klientów?

MByD
źródło
3
Jakie są szanse, że wyślesz kompilację nie utworzoną przy użyciu kompilatora optymalizującego? A jeśli tak się stanie, czy wpływ będzie miał znaczenie (zwłaszcza w kontekście wszystkich innych optymalizacji, których wtedy przegapisz)?
@delnan - Kod jest używany na wielu różnych platformach z wieloma różnymi kompilatorami. A jeśli chodzi o wpływ - może w niektórych przypadkach.
MByD
Powiedziano ci, że wolisz coś? To jak zmuszanie kogoś do jedzenia jedzenia z KFC, podczas gdy on nie lubi kurczaka.
prawej
@WTP - nie, nie byłem, chcę wiedzieć więcej, więc w przyszłości podejmowałbym lepsze decyzje.
MByD
W twoim drugim przykładzie jest TYPE_X_CUSTOMER nadal jest makro preprocesora?
detly

Odpowiedzi:

5

Myślę, że jest jedna zaleta używania #define, o której nie wspomniałeś, a mianowicie fakt, że możesz ustawić wartość w wierszu poleceń (a więc ustawić ją ze skryptu kompilacji w jednym kroku).

Poza tym ogólnie lepiej unikać makr. Nie szanują żadnego zakresu, co może powodować problemy. Tylko bardzo głupie kompilatory nie mogą zoptymalizować warunku na podstawie stałej czasowej kompilacji. Nie wiem, czy to dotyczy twojego produktu (na przykład ważne może być utrzymanie małego kodu na platformach osadzonych).

Tamás Szelei
źródło
W rzeczywistości dotyczy to systemów wbudowanych
MByD
Problem polega na tym, że systemy osadzone zwykle używają starożytnego kompilatora (na przykład gcc 2.95).
BЈовић
@VJo - GCC to szczęśliwy przypadek :)
MByD 24.11.11
Wypróbuj to. Jeśli zawiera nieużywany kod i jest to znaczny rozmiar, przejdź do definicji.
Tamás Szelei 24.11.11
Jeśli chcesz mieć możliwość ustawienia go za pomocą wiersza poleceń kompilatora, nadal użyłbym stałej w samym kodzie i tylko # zdefiniowałby wartość przypisaną do tej stałej.
CodesInChaos
12

Jeśli chodzi o wiele pytań, odpowiedź na to pytanie jest zależna . Zamiast powiedzieć, co jest lepsze, podałem raczej przykłady i cele, w których jeden jest lepszy od drugiego.

Zarówno preprocesor, jak i stały mają własne miejsca odpowiedniego użytkowania.

W przypadku procesora wstępnego kod jest usuwany przed czasem kompilacji. Dlatego najlepiej nadaje się do sytuacji, w których nie należy kompilować kodu . Może to wpłynąć na strukturę modułu, zależności i może pozwolić na wybór najlepszych segmentów kodu pod względem wydajności. W poniższych przypadkach kod należy podzielić tylko przy użyciu preprocesora.

  1. Kod wieloplatformowy:
    Na przykład, gdy kod jest kompilowany na różnych platformach, gdy kod jest zależny od określonych numerów wersji systemu operacyjnego (lub nawet wersji kompilatora - choć jest to bardzo rzadkie). Na przykład, gdy masz do czynienia z odpowiednikami kodu little-endien big-endien - muszą one być oddzielone od preprocesorów, a nie stałych. Lub jeśli kompilujesz kod dla systemu Windows i Linux, a niektóre wywołania systemowe są bardzo różne.

  2. Łatki eksperymentalne:
    Kolejnym przypadkiem, w którym powoduje to uzasadnienie, jest pewien kod eksperymentalny, który jest ryzykowny, lub niektóre główne moduły, które należy pominąć, które będą miały znaczące różnice w łączeniu lub wydajności. Powodem, dla którego chcielibyśmy wyłączyć kod za pomocą preprocesora zamiast ukrywania się pod if (), jest to, że możemy nie być pewni błędów wprowadzonych przez ten konkretny zestaw zmian i działamy w oparciu o eksperymenty. Jeśli to się nie powiedzie, nie możemy nic więcej zrobić poza wyłączeniem tego kodu w produkcji niż przepisywanie. Czasami idealnie nadaje się #if 0 do komentowania całego kodu.

  3. Radzenie sobie z zależnościami:
    Kolejny powód, dla którego możesz chcieć wygenerować Na przykład, jeśli nie chcesz obsługiwać obrazów JPEG, możesz pomóc pozbyć się kompilacji tego modułu / kodu pośredniczącego i ostatecznie biblioteka nie będzie (statycznie lub dynamicznie) linkować do tego moduł. Czasami uruchamiane są pakiety w ./configurecelu określenia dostępności takiej zależności i jeśli biblioteki nie są obecne (lub użytkownik nie chce włączyć), taka funkcja jest automatycznie wyłączana bez łączenia się z tą biblioteką. W tym przypadku zawsze korzystne jest automatyczne generowanie tych dyrektyw.

  4. Licencjonowanie:
    Jednym z bardzo interesujących przykładów dyrektywy preprocesorowej jest ffmpeg . Ma kodeki, które mogą potencjalnie naruszać patenty podczas jego używania. Jeśli pobierzesz źródło i skompilujesz, aby zainstalować, zostaniesz zapytany, czy chcesz tego uniknąć. Ukrywanie kodów w niektórych przypadkach, jeśli warunki nadal mogą wylądować w sądzie!

  5. Kod kopiuj-wklej:
    makra Aka. Nie jest to wskazówka do nadmiernego używania makr - wystarczy, że makra mają znacznie bardziej zaawansowany sposób na zastosowanie odpowiednika kopiowania z przeszłości . Ale używaj go z wielką ostrożnością; i użyj go, jeśli wiesz, co robisz. Stałe oczywiście nie mogą tego zrobić. Ale można również korzystać z inlinefunkcji, jeśli jest to łatwe do wykonania.

Kiedy więc używasz stałych?
Niemal wszędzie indziej.

  1. Neater flow code:
    Ogólnie rzecz biorąc, gdy używasz stałych, jest prawie nie do odróżnienia od zwykłych zmiennych, a zatem jest lepiej czytelny. Jeśli piszesz procedurę składającą się z 75 linii - mają 3 lub 4 linie po każdych 10 liniach z #ifdef BARDZO nie można odczytać . Prawdopodobnie podano podstawową stałą rządzoną przez #ifdef i używaj jej w naturalnym przepływie w każdym miejscu.

  2. Dobrze wcięty kod: wszystkie dyrektywy preprocesora, nigdy nie działają dobrze z kodem dobrze wciętym inaczej . Nawet jeśli twój kompilator pozwala na wcięcie #def, preprocesor Pre-ANSI C nie pozwalał na spację między początkiem linii a znakiem „#”; wiodące „#” zawsze musiało być umieszczone w pierwszej kolumnie.

  3. Konfiguracja:
    Innym powodem, dla którego stałe / lub zmienne mają sens, jest to, że mogą one łatwo ewoluować od bycia połączonymi z globalsami lub w przyszłości mogą zostać rozszerzone, aby uzyskać je z plików konfiguracyjnych.

Ostatnia rzecz:
nigdy NIE UŻYWAJ dyrektyw preprocesora w #ifdefcelu #endif przekroczenia zakresu lub { ... }. tzn. początek #ifdeflub koniec #endifpo różnych stronach{ ... } . To jest bardzo złe; może być mylące, czasem niebezpieczne.

To oczywiście nie jest wyczerpująca lista, ale pokazuje wyraźną różnicę, w przypadku której metody lepiej użyć. Tak naprawdę nie chodzi o to, co jest lepsze , ale zawsze bardziej naturalne jest użycie w danym kontekście.

Dipan Mehta
źródło
Dzięki za długą i szczegółową odpowiedź. Byłem świadomy większości rzeczy, o których wspomniałeś, i zostały one pominięte w pytaniu, ponieważ nie chodzi o to, czy w ogóle korzystać z możliwości preprocesora, ale o konkretny rodzaj użycia. Ale myślę, że podstawowa kwestia, o której mówiłeś, jest prawdziwa.
MByD,
1
„Nigdy NIE UŻYWAJ dyrektyw preprocesora #ifdef do #endif przekraczających zakres lub {...}”, zależy to od IDE. Jeśli IDE rozpoznaje nieaktywne bloki preprocesora i zwija je lub pokazuje innym kolorem, nie jest to mylące.
Abyx
@Abyx to prawda, że ​​IDE może pomóc. Jednak w wielu przypadkach, gdy ludzie rozwijają się na platformie * nix (Linux itp.) - wybór redaktorów itp. Jest wielu (VI, emacs i tak dalej). Więc ktoś może używać innego edytora niż twój.
Dipan Mehta
4

Czy Twoi klienci mają dostęp do Twojego kodu? Jeśli tak, to preprocesor może być lepszą opcją. Mamy coś podobnego i używamy flag czasu kompilacji dla różnych klientów (lub specyficznych funkcji klienta). Następnie skrypt może wyodrębnić kod specyficzny dla klienta, a my wysyłamy ten kod. Klienci nie są świadomi innych klientów ani innych funkcji specyficznych dla klienta.

Aditya Sehgal
źródło
3

Kilka dodatkowych punktów wartych wspomnienia:

  1. Kompilator może przetwarzać niektóre rodzaje wyrażeń stałych, których nie potrafi preprocesor. Na przykład preprocesor zazwyczaj nie ma możliwości oceny sizeof()typów innych niż pierwotne. W if()niektórych przypadkach może to wymusić użycie .

  2. Kompilator zignoruje większość problemów ze składnią w kodzie, który jest pomijany #if(niektóre problemy związane z preprocesorem mogą nadal powodować problemy), ale nalega na poprawność składniową pomijanego kodu if(). Tak więc, jeśli kod, który jest pomijany dla niektórych kompilacji, ale nie dla innych, staje się nieprawidłowy w wyniku zmian w innym miejscu pliku (np. Zmiana nazwy identyfikatora), generalnie będzie skrzeczał na wszystkich kompilacjach, jeśli jest wyłączony, if()ale nie, jeśli jest wyłączony przez #if. W zależności od tego, dlaczego kod jest pomijany, może to być dobre rozwiązanie.

  3. Niektóre kompilatory pominą nieosiągalny kod, podczas gdy inne nie; deklaracje o czasie przechowywania statycznego w obrębie nieosiągalnego kodu, w tym także literały łańcuchowe, mogą przydzielić miejsce, nawet jeśli żaden kod nigdy nie może wykorzystać miejsca w ten sposób przydzielonego.

  4. Makra mogą korzystać z if()testów wewnątrz nich, ale nie ma niestety mechanizmu umożliwiającego preprocesorowi wykonywanie dowolnej logiki warunkowej w makrze [zwróć uwagę, że w przypadku makr ? :operator często może być lepszy niż if, ale obowiązują te same zasady].

  5. #ifDyrektywa może być używany do sterowania co makra uzyskać określone.

Myślę, że if()często jest czystszy w większości przypadków, z wyjątkiem tych, które wiążą się z podejmowaniem decyzji na podstawie używanego kompilatora lub makr, które należy zdefiniować; niektóre z powyższych problemów mogą wymagać użycia #if, nawet w przypadkach, w których ifwydają się czystsze.

supercat
źródło
1

Krótko mówiąc: obawy przed dyrektywami preprocesora to w zasadzie 2, wiele inkluzji i być może mniej czytelny kod i bardziej tajemnicza logika.

Jeśli twój projekt jest niewielki i nie ma prawie żadnego dużego planu na przyszłość, myślę, że obie opcje oferują taki sam stosunek zalet i wad, ale jeśli twój projekt będzie ogromny, rozważ coś innego, jak napisanie osobnego pliku nagłówka, jeśli nadal chcesz korzystać z dyrektyw preprocesora, ale pamiętaj, że takie podejście zwykle sprawia, że ​​kod jest mniej czytelny i upewnij się, że nazwa tych stałych ma znaczenie dla logiki biznesowej samego programu.

Micro
źródło
Jest to świetna baza kodu, a definicje znajdują się w osobnych nagłówkach.
MByD