Używanie #ifdef do przełączania między różnymi typami zachowań podczas programowania

28

Czy dobrą praktyką jest używanie #ifdef podczas programowania do przełączania różnych typów zachowań? Na przykład chcę zmienić zachowanie istniejącego kodu, mam kilka pomysłów, jak zmienić zachowanie i konieczne jest przełączanie się między różnymi implementacjami, aby przetestować i porównać różne podejścia. Zwykle zmiany w kodzie są złożone i wpływają na różne metody w różnych plikach.

Zazwyczaj wprowadzam kilka identyfikatorów i robię coś takiego

void foo()
{
    doSomething1();
#ifdef APPROACH1
    foo_approach1();
#endif
    doSomething2();
#ifdef APPROACH2
    foo_approach2();
#endif
}

void bar()
{
    doSomething3();
#ifndef APPROACH3
    doSomething4();
#endif
    doSomething5();
#ifdef APPROACH2
    bar_approach2();
#endif
}

int main()
{
    foo();
    bar();
    return 0;
}

Pozwala to szybko przełączać się między różnymi podejściami i robić wszystko w jednej kopii kodu źródłowego. Czy to dobre podejście do rozwoju, czy jest lepsza praktyka?

Yury Shkliaryk
źródło
2
Ponieważ mówisz o rozwoju, uważam, że musisz zrobić wszystko, co uważasz za łatwe, aby przełączyć się i eksperymentować z różnymi implementacjami. Jest to bardziej jak osobiste preferencje podczas opracowywania, a nie najlepsza praktyka rozwiązania konkretnego problemu.
Emerson Cardoso,
1
Polecam stosowanie wzorca strategii lub dobrego polimorfizmu, ponieważ pomaga to zachować pojedynczy punkt wtyczki dla przełączalnego zachowania.
pmf
4
Pamiętaj, że niektóre IDE nie oceniają niczego w #ifdefblokach, jeśli blok jest wyłączony. Natknęliśmy się na przypadki, w których kod może łatwo zestarzeć się i nie skompilować, jeśli nie budujesz rutynowo wszystkich ścieżek.
Berin Loritsch,
Spójrz na tę odpowiedź, której udzieliłem na inne pytanie. Przedstawia kilka sposobów na uczynienie wielu #ifdefsmniej uciążliwymi.
user1118321,

Odpowiedzi:

9

Wolę używać gałęzi kontroli wersji dla tego przypadku użycia. Pozwala to na różnicowanie między implementacjami, prowadzenie osobnej historii dla każdej z nich, a kiedy już podejmiesz decyzję i musisz usunąć jedną z wersji, po prostu odrzucisz tę gałąź zamiast poddawać się podatnej na błędy edycji.

Karl Bielefeldt
źródło
gitjest szczególnie biegły w tego typu rzeczach. Może nie tyle w przypadku svn, hgczy inni, ale może być jeszcze zrobione.
twalberg,
To też była moja pierwsza myśl. „Chcesz zadzierać z czymś innym?” git branch!
Wes Toleman,
42

Kiedy trzymasz młotek, wszystko wygląda jak gwóźdź. To kuszące, gdy wiesz, jak to #ifdefdziała, aby użyć go jako środka do uzyskania niestandardowego zachowania w twoim programie. Wiem, bo popełniłem ten sam błąd.

Odziedziczyłem starszy program napisany w MFC C ++, który już służył #ifdefdo definiowania wartości specyficznych dla platformy. Oznaczało to, że mogłem skompilować mój program do użycia na platformie 32-bitowej lub 64-bitowej, po prostu definiując (lub w niektórych przypadkach nie definiując) określone wartości makr.

Powstał wtedy problem, że musiałem napisać niestandardowe zachowanie klienta. Mógłbym stworzyć gałąź i stworzyć osobną bazę kodu dla klienta, ale to spowodowałoby piekło serwisowe. Mógłbym również zdefiniować wartości konfiguracyjne do odczytania przez program podczas uruchamiania i użyć tych wartości do określenia zachowania, ale musiałbym wtedy utworzyć niestandardowe ustawienia, aby dodać prawidłowe wartości konfiguracyjne do plików konfiguracyjnych dla każdego klienta.

Kusiło mnie i poddałem się. Napisałem #ifdefsekcje w moim kodzie, aby rozróżnić różne zachowania. Nie popełnij błędu, na początku nie było to przesadą. Wprowadzono bardzo niewielkie zmiany w zachowaniu, które pozwoliły mi redystrybuować wersje programu wśród naszych klientów i nie muszę mieć więcej niż jednej wersji bazy kodu.

Z biegiem czasu stało się to piekło konserwacyjnych i tak , ponieważ program nie będzie już zachowywał się konsekwentnie w całej planszy. Gdybym chciał przetestować wersję programu, musiałem koniecznie wiedzieć, kim był klient. Kod, choć starałem się zredukować go do jednego lub dwóch plików nagłówkowych, był bardzo zagracony, a szybkie podejście, które #ifdefzapewniło, oznaczało, że takie rozwiązania rozprzestrzeniły się w całym programie jak złośliwy rak.

Od tego czasu nauczyłem się mojej lekcji i ty też powinieneś. Użyj go, jeśli absolutnie musisz, i używaj go ściśle do zmian platformy. Najlepszym sposobem podejścia do różnic w zachowaniu między programami (a zatem i klientami) jest zmiana tylko konfiguracji ładowanej podczas uruchamiania. Program pozostaje spójny i zarówno łatwiej jest go czytać, jak i debugować.

Neil
źródło
co z wersjami debugowania, np. „jeśli debuguj, zdefiniuj zmienną x ...” wydaje się, że może to być przydatne do takich rzeczy, jak rejestrowanie, ale może również całkowicie zmienić sposób działania programu, gdy debugowanie jest włączone, a kiedy nie jest .
kiedy
8
@ snb Myślałem o tym. Nadal wolę móc zmieniać plik konfiguracyjny i rejestrować go bardziej szczegółowo. W przeciwnym razie coś pójdzie nie tak z produkcyjnym programem i nie będzie można go debugować bez całkowitego zastąpienia pliku wykonywalnego. Nawet w idealnych sytuacjach jest to mniej niż pożądane. ;)
Neil,
O tak, byłoby o wiele bardziej idealne, gdyby nie trzeba było rekompilować w celu debugowania, nie myślałem o tym!
kiedy
9
Aby zobaczyć ekstremalny przykład tego, co opisujesz, spójrz na drugi akapit pod podtytułem „Problem utrzymania” w tym artykule, dlaczego stwardnienie rozsiane osiągnęło punkt, w którym kilka lat temu musieli przepisać większość swojego środowiska uruchomieniowego C . blogs.msdn.microsoft.com/vcblog/2014/06/10/…
Dan Neely
2
@snb Większość bibliotek rejestrujących zakłada mechanizm poziomu rejestrowania. Jeśli chcesz, aby pewne informacje były rejestrowane podczas debugowania, logujesz je przy niskim poziomie logowania (zwykle „Debugowanie” lub „Pełne”). Następnie aplikacja ma parametr konfiguracyjny , który informuje, jaki poziom należy zarejestrować. Tak więc odpowiedzią jest nadal konfiguracja tego problemu. Ma to również ogromną zaletę, że można włączyć ten niski poziom rejestrowania w środowisku klienta .
jpmc26,
21

Tymczasowo nie ma nic złego w tym, co robisz (powiedzmy przed odprawą): to świetny sposób na przetestowanie różnych kombinacji technik lub zignorowanie części kodu (choć mówi o problemach samo w sobie).

Ale słowo ostrzeżenia: nie utrzymuj, że gałęzie #ifdef są nieco bardziej frustrujące niż marnowanie czasu na czytanie tej samej rzeczy zaimplementowane na cztery różne sposoby, tylko po to, aby dowiedzieć się, który powinienem czytać .

Czytanie nad #ifdef wymaga wysiłku, ponieważ musisz pamiętać, aby go pominąć! Nie rób tego trudniej niż to absolutnie musi być.

Używaj #ifdefs tak oszczędnie, jak to możliwe. Istnieją ogólnie sposoby, aby to zrobić w środowisku programistycznym w celu uzyskania trwałych różnic, takich jak kompilacje debugowania / wydania lub dla różnych architektur.

Napisałem funkcje biblioteki, które były zależne od dołączonych wersji bibliotek, które wymagały podziału #ifdef. Czasami więc może to być jedyny lub najłatwiejszy sposób, ale nawet wtedy powinieneś być zmartwiony ich utrzymaniem.

Erdrik Ironrose
źródło
1

Korzystanie z #ifdefs w ten sposób bardzo utrudnia odczytanie kodu.

Więc nie, nie używaj #ifdefs w ten sposób.

Może być mnóstwo argumentów, dlaczego nie użyć ifdefs, dla mnie ten wystarczy.

void foo()
{
    doSomething1();
#ifdef APPROACH1
    foo_approach1();
#endif
    doSomething2();
#ifdef APPROACH2
    foo_approach2();
#endif
}

Potrafi robić wiele rzeczy:

void foo()
{
    doSomething1();
    doSomething2();
}

void foo()
{
    doSomething1();
    foo_approach1();
    doSomething2();
}

void foo()
{
    doSomething1();
    doSomething2();
    foo_approach2();
}

void foo()
{
    doSomething1();
    foo_approach1();
    doSomething2();
    foo_approach2();
}

Wszystko w zależności od tego, jakie podejścia są zdefiniowane, czy nie. To, co robi, nie jest absolutnie jasne na pierwszy rzut oka.

Pieter B.
źródło