Dans cet article, je vais vous raconter comment j'ai transféré les entreprises sur lesquelles j'ai travaillé pendant plus de cinq ans de la gestion de projets de microcontrôleurs en C vers C ++ et ce qui en est ressorti (spoiler: tout va mal).
Un peu de toi
J'ai commencé à écrire sous les microcontrôleurs C, n'ayant qu'une expérience scolaire avec Pascal, puis j'ai étudié l'assembleur et j'ai passé environ 3 ans à étudier diverses architectures de microcontrôleurs et leurs périphériques. Puis il y a eu l'expérience du vrai travail en C # et C ++ avec leur étude parallèle, qui a duré plusieurs années. Après cette période, je suis retourné encore et longtemps à la programmation de microcontrôleurs, disposant déjà des bases théoriques nécessaires pour travailler sur des projets réels.
Première année
Je n'avais rien contre le style procédural de C, cependant, l'entreprise qui a commencé ma pratique réelle sur des projets réels a utilisé la «programmation C dans un style orienté objet». Cela ressemblait à quelque chose comme ça.
typedef const struct _uart_init { USART_TypeDef *USARTx; uint32_t baudrate; ... } uart_cfg_t; int uart_init (uart_cfg_t *cfg); int uart_start_tx (int fd, void *d, uint16_t l); int uart_tx (int fd, void *d, uint16_t l, uint32_t timeout);
Cette approche présente les avantages suivants:
- le code est resté le code C. Les avantages suivants en découlent:
- il est plus facile de contrôler les «objets», car il est facile de retracer qui et où provoque quoi et dans quelle séquence (à l'exception des interruptions, mais pas dans cet article);
- pour stocker le "pointeur sur l'objet" il suffit de se souvenir du fd retourné;
- si «l'objet» a été supprimé, alors lorsque vous essayez de l'utiliser, vous recevrez une erreur correspondante dans la valeur de retour de la fonction;
- l'abstraction de tels objets sur le HAL qui y est utilisé a permis d'écrire des objets personnalisables pour la tâche à partir de sa propre structure d'initialisation (et dans le cas d'un manque de fonctionnalité HAL, on pourrait masquer l'accès aux registres à l'intérieur des "objets").
Inconvénients:
- si quelqu'un a supprimé «l'objet», puis en a créé un nouveau d'un type différent, il peut arriver que le nouveau obtienne le fd de l'ancien et qu'aucun autre comportement ne soit déterminé. Ce comportement pourrait être facilement modifié au prix d'une petite consommation de mémoire pour une liste chaînée au lieu d'utiliser un tableau avec une «valeur-clé» (un tableau pour chaque index fd stockait un pointeur sur la structure de l'objet).
- il était impossible de marquer statiquement la mémoire sous "objets globaux". Étant donné que dans la plupart des applications, les «objets» ont été créés une fois et n'ont pas été supprimés, cela ressemblait à une «béquille». Ici, lors de la création d'un objet, il serait possible de passer un pointeur sur sa structure interne, qui était allouée statiquement lors de la mise en page, mais cela perturberait encore plus le code d'initialisation et romprait l'encapsulation.
Lorsqu'on leur a demandé pourquoi C ++ n'avait pas été sélectionné lors de la construction de l'infrastructure entière, ils ont répondu quelque chose comme ceci: «Eh bien, C ++ entraîne des coûts supplémentaires élevés, des coûts de mémoire incontrôlés, ainsi qu'un fichier de micrologiciel exécutable volumineux. Peut-être qu'ils avaient raison. En effet, au moment du début de la conception, il n'y avait que GCC 3.0.5, qui ne brillait pas avec une convivialité particulière au C ++ (nous devons encore travailler avec lui pour écrire des programmes sous QNX6). Il n'y avait pas de constexpr et C ++ 11/14, vous permettant de créer des objets globaux, qui étaient essentiellement des données dans la zone .data, calculées au stade de la compilation et des méthodes pour eux.
À la question, pourquoi ne pas écrire sur les registres - j'ai obtenu une réponse claire que l'utilisation des "objets" vous permet de configurer le même type d'application "en une journée".
Réalisant tout cela et réalisant que maintenant C ++ n'est plus le même qu'avec GCC 3.0.5, j'ai commencé à réécrire la partie principale de la fonctionnalité en C ++. Pour commencer, travaillez avec les périphériques matériels du microcontrôleur, puis les périphériques des périphériques externes. En fait, ce n'était qu'un shell plus pratique que ce qui était disponible à l'époque.
Deuxième et troisième années
J'ai réécrit tout ce dont j'avais besoin pour mes projets en C ++ et j'ai continué à écrire de nouveaux modules tout de suite en C ++. Cependant, il ne s'agissait que de shells sur C. Ayant réalisé que je n'utilisais pas assez C ++, j'ai commencé à utiliser ses points forts: modèles, classes d'en-tête uniquement, constexpr, etc. Tout allait bien.
Quatrième et cinquième année
- tous les objets sont globaux et incluent des liens entre eux au stade de la compilation (selon l'architecture du projet);
- tous les objets reçoivent de la mémoire au stade de la mise en page;
- par objet de classe pour chaque broche;
- un objet qui encapsule toutes les broches pour les initialiser avec une seule méthode;
- un objet de contrôle RCC qui encapsule tous les objets qui se trouvent sur les bus matériels;
- Le projet de convertisseur CAN <-> RS485 selon le protocole client contient 60 objets;
- dans le cas où quelque chose se trouve à ce niveau au niveau HAL ou classe d'un objet, vous devez non seulement «résoudre le problème», mais aussi penser à le résoudre pour que ce correctif fonctionne sur toutes les configurations possibles de ce module ;
- les modèles et constexpr utilisés ne peuvent pas être calculés avant de visualiser les fichiers map, asm et bin du firmware final (ou de lancer le débogage dans le microcontrôleur);
- en cas d'erreur dans le modèle, un message d'une longueur d'un tiers de la configuration du projet de GCC est émis. En lire et en comprendre quelque chose est une réalisation distincte.
Résumé
Maintenant, je comprends ce qui suit:
- l'utilisation de «constructeurs de modules universels» complique inutilement le programme. Il est beaucoup plus facile d'ajuster les registres de configuration pour un nouveau projet que de se plonger dans les relations entre les objets, puis aussi dans la bibliothèque HAL;
- n'ayez pas peur d'utiliser C ++ de peur qu'il "engloutisse beaucoup de mémoire" ou "soit moins optimisé que C". Non, ça ne l'est pas. Vous devez avoir peur que l'utilisation d'objets et de nombreuses couches d'abstraction rende le code illisible, et le débogage sera un exploit héroïque;
- si vous n'utilisez rien de «compliquant», comme les modèles, l'héritage et d'autres charmes attrayants de C ++, alors pourquoi utiliser C ++? Juste pour le bien des objets? Est-ce que ça vaut le coup? Et pour des objets globaux statiques sans utiliser new / delete interdits sur certains projets?
En résumé, nous pouvons dire que l'apparente simplicité d'utilisation de C ++ s'est avérée être juste une excuse pour augmenter à plusieurs reprises la complexité du projet sans aucun gain de vitesse ou de mémoire.