Omettre les «destructeurs» en C, c'est aller trop loin avec YAGNI?

9

Je travaille sur une application moyenne embarquée en C utilisant des techniques de type OO. Mes "classes" sont des modules .h / .c utilisant des structures de données et des structures de pointeurs de fonction pour émuler l'encapsulation, le polymorphisme et l'injection de dépendances.

Maintenant, on pourrait s'attendre à ce qu'une myModule_create(void)fonction vienne avec une myModule_destroy(pointer)contrepartie. Mais le projet étant intégré, les ressources qui sont instanciées de manière réaliste ne devraient jamais être libérées.

Je veux dire, si j'ai 4 ports série UART et que je crée 4 instances UART avec leurs broches et paramètres requis, il n'y a absolument aucune raison de vouloir détruire UART # 2 à un moment donné pendant l'exécution.

Donc, en suivant le principe YAGNI (vous n'en aurez pas besoin), dois-je omettre les destructeurs? Cela me semble extrêmement étrange mais je ne peux pas penser à une utilisation pour eux; les ressources sont libérées lorsque l'appareil s'éteint.

Asics
la source
4
Si vous ne fournissez pas un moyen de disposer l'objet, vous passez un message clair qu'ils ont une durée de vie "infinie" une fois créés. Si cela a du sens pour votre candidature, je dis: faites-le.
glampert
4
Si vous allez aussi loin en couplant le type à votre cas d'utilisation particulier, pourquoi même avoir une myModule_create(void)fonction? Vous pouvez simplement coder en dur les instances spécifiques que vous prévoyez d'utiliser dans l'interface que vous exposez.
Doval
@Doval j'y ai pensé. Je suis un stagiaire qui utilise des parties et des morceaux de code de mon superviseur, donc j'essaie de jongler avec «bien faire», expérimenter le style OO en C pour l'expérience et la cohérence avec les normes de l'entreprise.
Asics
2
@glampert le cloue; J'ajouterais que vous devriez nettoyer la durée de vie attendue-infinie dans la documentation de la fonction de création.
Blrfl

Réponses:

11

Si vous ne fournissez pas un moyen de disposer l'objet, vous passez un message clair qu'ils ont une durée de vie "infinie" une fois créés. Si cela a du sens pour votre candidature, je dis: faites-le.

Glampert a raison; il n'y a pas besoin de destructeurs ici. Ils ne feraient que créer du ballonnement de code et un piège pour les utilisateurs (l'utilisation d'un objet après l'appel de son destructeur est un comportement non défini).

Cependant, vous devez être sûr qu'il n'est vraiment pas nécessaire de jeter les objets. Par exemple, avez-vous besoin d'un objet pour un UART qui n'est pas actuellement utilisé?

Demi
la source
3

Le moyen le plus simple que j'ai trouvé pour détecter les fuites de mémoire est de pouvoir quitter proprement votre application. De nombreux compilateurs / environnements offrent un moyen de vérifier la mémoire qui est toujours allouée lorsque votre application se ferme. Si aucun n'est fourni, il existe généralement un moyen d'ajouter du code juste avant de quitter, ce qui peut le comprendre.

Donc, je fournirais certainement des constructeurs, des destructeurs et une logique d'arrêt même dans un système embarqué qui "théoriquement" ne devrait jamais sortir pour faciliter la détection de fuite de mémoire seule. En réalité, la détection de fuite de mémoire est encore plus importante si le code d'application ne doit jamais quitter.

Tremper
la source
Notez que cela ne s'applique pas si la seule fois où vous effectuez des allocations est au démarrage, ce qui est un modèle que je considérerais sérieusement dans les périphériques de contrainte de mémoire.
CodesInChaos
@Codes: Il n'y a aucune raison de vous limiter. Bien que vous puissiez proposer un excellent design qui pré-alloue de la mémoire au démarrage, lorsque les personnes après vous qui ne sont pas au courant de ce grand schéma ou ne voient pas l'importance de celui-ci, elles alloueront de la mémoire sur le voler et voilà votre conception. Faites-le bien et allouez / désallouez et vérifiez que ce que vous avez mis en œuvre fonctionne réellement. Si vous avez vraiment un périphérique contraint en mémoire, ce qui est généralement fait est de remplacer les nouveaux blocs d'allocation opérateur / malloc et de pré-réservation.
Dunk
3

Dans mon développement, qui fait un usage intensif de types de données opaques pour favoriser une approche de type OO, je me suis trop débattu avec cette question. Au début, j'étais décidément dans le camp de l'élimination du destructeur du point de vue YAGNI, ainsi que du point de vue du "code mort" de la MISRA. (J'avais beaucoup d'espace pour les ressources, ce n'était pas une considération.)

Cependant ... l'absence d'un destructeur peut rendre les tests plus difficiles, comme dans les tests unitaires / d'intégration automatisés. Classiquement, chaque test doit prendre en charge une configuration / démontage afin que les objets puissent être créés, manipulés, puis détruits. Ils sont détruits afin d'assurer un point de départ propre et intact pour le test suivant. Pour ce faire, la classe a besoin d'un destructeur.

Par conséquent, d'après mon expérience, le «n'est pas» dans YAGNI se révèle être un «sont» et j'ai fini par créer des destructeurs pour chaque classe, que je pensais en avoir besoin ou non. Même si j'ai sauté les tests, au moins un destructeur correctement conçu existe pour le pauvre slob qui me suit en aura un. (Et, à une valeur bien moindre, il rend le code plus réutilisable dans la mesure où il peut être utilisé dans un environnement où il serait détruit.)

Bien que cela concerne YAGNI, cela ne concerne pas le code mort. Pour cela, je trouve qu'une macro de compilation conditionnelle quelque chose comme #define BUILD_FOR_TESTING permet au destructeur d'être éliminé de la version de production finale.

Pour ce faire, vous disposez d'un destructeur pour les tests / réutilisation future, et vous répondez aux objectifs de conception de YAGNI et aux règles "sans code mort".

Greg Willits
la source
Faites attention à # ifdef'er votre code test / prod. Il est raisonnablement sûr lorsqu'il est appliqué à une fonction entière, comme vous le décrivez, car si la fonction est en fait nécessaire, la compilation échouera. Cependant, l'utilisation de #ifdef inline dans une fonction est beaucoup plus risquée car vous testez maintenant un chemin de code différent de celui qui s'exécute dans prod.
Kevin
0

Vous pourriez avoir un destructeur sans opération, quelque chose comme

  void noop_destructor(void*) {};

puis définissez le destructeur d' Uartutiliser peut - être

  #define Uart_destructor noop_destructor

(ajoutez la fonte appropriée si nécessaire)

N'oubliez pas de documenter. Peut-être que tu veux même

 #define Uart_destructor abort

Alternativement, cas spécial dans le code commun appelant destructeur le cas où la fonction de pointeur du destructeur est NULLd'éviter de l'appeler.

Basile Starynkevitch
la source