Pominięcie „destruktorów” w C prowadzi zbyt daleko od YAGNI?

9

Pracuję nad aplikacją średnio osadzoną w C przy użyciu technik podobnych do OO. Moje „klasy” to moduły .h / .c wykorzystujące struktury danych i struktury wskaźników funkcji do emulacji enkapsulacji, polimorfizmu i wstrzykiwania zależności.

Teraz można oczekiwać, że myModule_create(void)funkcja będzie miała myModule_destroy(pointer)odpowiednik. Ale ponieważ projekt jest osadzony, zasoby, które są tworzone realistycznie, nigdy nie powinny zostać uwolnione.

Mam na myśli, że jeśli mam 4 porty szeregowe UART i utworzę 4 instancje UART z wymaganymi pinami i ustawieniami, nie ma absolutnie żadnego powodu, aby kiedykolwiek chcieć zniszczyć UART # 2 w pewnym momencie podczas uruchamiania.

Czy więc zgodnie z zasadą YAGNI (nie będziesz jej potrzebować), czy powinienem pominąć destruktory? Wydaje mi się to bardzo dziwne, ale nie mogę wymyślić dla nich zastosowania; zasoby są zwalniane po wyłączeniu urządzenia.

Asics
źródło
4
Jeśli nie zapewnisz sposobu na pozbycie się obiektu, przekazujesz jasny komunikat, że mają one „nieskończone” życie po utworzeniu. Jeśli ma to sens dla twojej aplikacji, mówię: zrób to.
glampert
4
Jeśli zamierzasz posunąć się tak daleko, łącząc typ z konkretnym przypadkiem użycia, dlaczego w ogóle masz jakąś myModule_create(void)funkcję? Możesz po prostu zakodować określone wystąpienia, których spodziewasz się użyć w ujawnianym interfejsie.
Doval
@Doval Myślałem o tym. Jestem stażystą używającym części i fragmentów kodu od mojego przełożonego, więc staram się żonglować „robieniem tego dobrze”, eksperymentowaniem stylu OO w C w celu uzyskania doświadczenia i zgodności ze standardami firmy.
Asics
2
@glampert przybija paznokcie; Dodam, że powinieneś wyczyścić oczekiwany nieskończony czas życia w dokumentacji funkcji tworzenia.
Blrfl,

Odpowiedzi:

11

Jeśli nie zapewnisz sposobu na pozbycie się obiektu, przekazujesz jasny komunikat, że mają one „nieskończone” życie po utworzeniu. Jeśli ma to sens dla twojej aplikacji, mówię: zrób to.

Glampert ma rację; tutaj nie ma potrzeby stosowania destruktorów. Tworzyliby po prostu rozdęcie kodu i pułapkę dla użytkowników (używanie obiektu po wywołaniu destruktora jest niezdefiniowanym zachowaniem).

Należy jednak upewnić się, że naprawdę nie ma potrzeby usuwania obiektów. Na przykład, czy potrzebujesz obiektu dla UART, który nie jest obecnie używany?

Demi
źródło
3

Najłatwiejszym sposobem na wykrycie wycieków pamięci jest czyste zamknięcie aplikacji. Wiele kompilatorów / środowisk umożliwia sprawdzanie ilości pamięci, która jest nadal przydzielana po zamknięciu aplikacji. Jeśli nie jest dostarczony, zwykle jest sposób, aby dodać kod tuż przed wyjściem, który może to rozgryźć.

Tak więc z pewnością zapewniłbym konstruktory, destruktory i logikę zamykania nawet w systemie osadzonym, który „teoretycznie” nigdy nie powinien wychodzić dla łatwego wykrywania wycieków pamięci. W rzeczywistości wykrywanie wycieków pamięci jest jeszcze ważniejsze, jeśli kod aplikacji nigdy nie powinien wyjść.

Maczać
źródło
Zauważ, że nie dotyczy to sytuacji, gdy alokacja odbywa się tylko podczas uruchamiania, co jest poważnym wzięciem pod uwagę w urządzeniach z ograniczeniami pamięci.
CodesInChaos
@Codes: Nie ma powodu, aby się ograniczać. Chociaż możesz wymyślić świetny projekt, który wstępnie alokuje pamięć podczas uruchamiania, gdy ludzie po tobie, którzy nie są wtajemniczeni w ten wielki program lub nie dostrzegają jego znaczenia, będą alokować pamięć na leć i tam idzie twój projekt. Po prostu zrób to dobrze i przydziel / cofnij przydzielenie i sprawdź, czy to, co zaimplementowałeś, faktycznie działa. Jeśli naprawdę masz urządzenie z ograniczoną pamięcią, wówczas zwykle wykonuje się operację zastępowania nowego operatora / centrum handlowego i zachowania bloków alokacji.
Dunk
3

W moim rozwoju, który szeroko wykorzystuje nieprzejrzyste typy danych do promowania podejścia podobnego do OO, również zmagałem się z tym pytaniem. Początkowo byłem zdecydowanie w obozie eliminacji destruktora z perspektywy YAGNI, a także perspektywy „martwego kodu” MISRA. (Miałem dużo miejsca na zasoby, to nie było wzięcie pod uwagę.)

Jednak ... brak destruktora może utrudnić testowanie, tak jak w przypadku automatycznych testów jednostkowych / integracyjnych. Konwencjonalnie każdy test powinien obsługiwać konfigurację / porzucenie, aby obiekty mogły być tworzone, manipulowane, a następnie niszczone. Są niszczone, aby zapewnić czysty, nieskażony punkt wyjścia do następnego testu. Aby to zrobić, klasa potrzebuje destruktora.

Dlatego, z mojego doświadczenia, „aint't” w YAGNI okazuje się być „są” i ostatecznie stworzyłem niszczyciele dla każdej klasy, niezależnie od tego, czy myślałem, że będę jej potrzebować, czy nie. Nawet jeśli pominąłem testowanie, istnieje przynajmniej poprawnie zaprojektowany destruktor dla biednego sloba, który za mną podąża. (I, przy znacznie mniejszej wartości, sprawia, że ​​kod jest bardziej użyteczny, ponieważ można go używać w środowisku, w którym zostałby zniszczony.)

Chociaż dotyczy YAGNI, nie dotyczy martwego kodu. W tym celu stwierdzam, że warunkowe makro kompilacji, takie jak #define BUILD_FOR_TESTING, pozwala wyeliminować destruktor z ostatecznej wersji produkcyjnej.

Robisz to w ten sposób: masz destruktor do testowania / przyszłego ponownego wykorzystania i spełniasz cele projektowe YAGNI i zasady „bez martwego kodu”.

Greg Willits
źródło
Uważaj na # ifdef'ing swojego kodu test / prod. Jak opisano, jest dość bezpieczny, gdy stosuje się go do całej funkcji, ponieważ jeśli funkcja jest faktycznie potrzebna, kompilacja się nie powiedzie. Jednak użycie #ifdef wewnątrz funkcji jest o wiele bardziej ryzykowne, ponieważ testujesz teraz inną ścieżkę kodową niż to, co działa w prod.
Kevin
0

Możesz mieć destruktor bez oporu, coś w rodzaju

  void noop_destructor(void*) {};

następnie ustaw destruktor Uartbyć może za pomocą

  #define Uart_destructor noop_destructor

(dodaj odpowiednią obsadę, jeśli jest potrzebna)

Nie zapomnij udokumentować. Może nawet chcesz

 #define Uart_destructor abort

Alternatywnie, specjalny przypadek we wspólnym kodzie wywołującym destruktor przypadek, gdy funkcja wskaźnika destruktora ma na NULLcelu uniknięcie jego wywołania.

Basile Starynkevitch
źródło