Il n'y a aucun avantage auquel je peux penser (mais voir la note à JasonS en bas), encapsulant une ligne de code comme une fonction ou un sous-programme. Sauf peut-être que vous pouvez nommer la fonction quelque chose de «lisible». Mais vous pouvez tout aussi bien commenter la ligne. Et comme le fait d'encapsuler une ligne de code dans une fonction coûte de la mémoire de code, de l'espace de pile et du temps d'exécution, il me semble que c'est surtout contre-productif. En situation d'enseignement? Cela pourrait avoir un certain sens. Mais cela dépend de la classe d'élèves, de leur préparation préalable, du programme et de l'enseignant. Surtout, je pense que ce n'est pas une bonne idée. Mais c'est mon opinion.
Ce qui nous amène à l'essentiel. Votre vaste domaine de questions est, depuis des décennies, un sujet de débat et reste à ce jour un sujet de débat. Donc, au moins en lisant votre question, il me semble que c'est une question d'opinion (comme vous l'avez posée).
Il pourrait être détourné d'être aussi fondé sur l'opinion qu'il l'est, si vous deviez être plus détaillé sur la situation et décrire soigneusement les objectifs que vous teniez comme principaux. Mieux vous définissez vos outils de mesure, plus les réponses peuvent être objectives.
D'une manière générale, vous souhaitez effectuer les opérations suivantes pour tout codage. (Pour ci-dessous, je suppose que nous comparons différentes approches qui atteignent toutes les objectifs. De toute évidence, tout code qui ne parvient pas à effectuer les tâches nécessaires est pire que le code qui réussit, quelle que soit la façon dont il est écrit.)
- Soyez cohérent dans votre approche, afin qu'une autre lecture de votre code puisse développer une compréhension de votre approche du processus de codage. Être incohérent est probablement le pire crime possible. Non seulement cela rend la tâche difficile pour les autres, mais cela rend difficile pour vous de revenir au code des années plus tard.
- Dans la mesure du possible, essayez d'arranger les choses afin que l'initialisation des différentes sections fonctionnelles puisse être effectuée sans égard à la commande. Lorsque la commande est requise, si elle est due à un couplage étroit de deux sous-fonctions hautement liées, envisagez une seule initialisation pour les deux afin qu'elle puisse être réorganisée sans causer de dommages. Si cela n'est pas possible, documentez l'exigence de commande d'initialisation.
- Si possible, encapsulez les connaissances dans un seul endroit. Les constantes ne doivent pas être dupliquées partout dans le code. Les équations qui résolvent une variable doivent exister en un et un seul endroit. Etc. Si vous vous retrouvez à copier et coller un ensemble de lignes qui exécutent certains comportements nécessaires à divers endroits, envisagez un moyen de capturer ces connaissances en un seul endroit et de les utiliser si nécessaire. Par exemple, si vous avez une structure d'arbre qui doit être parcouru d'une manière spécifique, ne pasrépliquez le code d'arborescence à chaque endroit où vous devez parcourir les nœuds de l'arborescence. Au lieu de cela, capturez la méthode de marche dans les arbres en un seul endroit et utilisez-la. De cette façon, si l'arbre change et que la méthode de marche change, vous n'avez qu'un seul endroit à vous soucier et tout le reste du code "fonctionne bien".
- Si vous étalez toutes vos routines sur une énorme feuille de papier plate, avec des flèches les reliant comme elles sont appelées par d'autres routines, vous verrez dans chaque application qu'il y aura des "grappes" de routines qui ont beaucoup, beaucoup de flèches entre eux mais seulement quelques flèches en dehors du groupe. Il y aura donc des limites naturelles de routines étroitement couplées et des connexions faiblement couplées entre d'autres groupes de routines étroitement couplées. Utilisez ce fait pour organiser votre code en modules. Cela limitera considérablement la complexité apparente de votre code.
Ce qui précède est généralement vrai pour tous les codages. Je n'ai pas discuté de l'utilisation des paramètres, des variables globales locales ou statiques, etc. La raison en est que pour la programmation intégrée, l'espace d'application impose souvent de nouvelles contraintes extrêmes et très importantes et il est impossible de les discuter toutes sans discuter de chaque application intégrée. Et cela ne se produit pas ici, de toute façon.
Ces contraintes peuvent être n'importe lesquelles (et plus):
- Limitations de coûts sévères nécessitant des microcontrôleurs extrêmement primitifs avec une mémoire RAM minuscule et presque aucun nombre de broches d'E / S. Pour ceux-ci, de nouveaux ensembles de règles entiers s'appliquent. Par exemple, vous devrez peut-être écrire du code d'assembly car il n'y a pas beaucoup d'espace de code. Vous devrez peut-être utiliser UNIQUEMENT des variables statiques car l'utilisation de variables locales est trop coûteuse et prend trop de temps. Vous devrez peut-être éviter l'utilisation excessive de sous-programmes car (par exemple, certaines parties Microchip PIC), il n'y a que 4 registres matériels dans lesquels stocker les adresses de retour des sous-programmes. Il vous faudra donc peut-être "aplatir" considérablement votre code. Etc.
- Limitations de puissance sévères nécessitant un code soigneusement conçu pour démarrer et arrêter la plupart des microcontrôleurs et imposant de sérieuses limitations sur le temps d'exécution du code lorsqu'il fonctionne à pleine vitesse. Encore une fois, cela peut parfois nécessiter un codage d'assemblage.
- Exigences de calendrier sévères. Par exemple, il y a des moments où j'ai dû m'assurer que la transmission d'un 0 à drain ouvert devait prendre EXACTEMENT le même nombre de cycles que la transmission d'un 1. Et que l'échantillonnage de cette même ligne devait également être effectué avec une phase relative exacte à ce moment. Cela signifiait que C ne pouvait pas être utilisé ici. La SEULE manière possible de faire cette garantie est d'élaborer soigneusement le code d'assemblage. (Et même alors, pas toujours sur tous les modèles ALU.)
Etc. (Le code de câblage pour l'instrumentation médicale vitale a également tout un monde.)
Le résultat ici est que le codage intégré n'est souvent pas gratuit pour tous, où vous pouvez coder comme vous le feriez sur un poste de travail. Il existe souvent des raisons de concurrence sévères pour une grande variété de contraintes très difficiles. Et ceux - ci peuvent fortement argumenter contre les plus traditionnels et actions réponses.
En ce qui concerne la lisibilité, je trouve que le code est lisible s'il est écrit d'une manière cohérente que je peux apprendre en le lisant. Et là où il n'y a pas de tentative délibérée d'obscurcir le code. Il n'y a vraiment pas besoin de beaucoup plus.
Le code lisible peut être assez efficace et il peut répondre à toutes les exigences ci-dessus que j'ai déjà mentionnées. L'essentiel est que vous compreniez parfaitement ce que chaque ligne de code que vous écrivez produit au niveau de l'assemblage ou de la machine, au fur et à mesure que vous la codez. C ++ impose une lourde charge au programmeur ici car il existe de nombreuses situations où des extraits de code C ++ identiques génèrent en fait différents extraits de code machine qui ont des performances très différentes. Mais C, en général, est surtout un langage «ce que vous voyez est ce que vous obtenez». C'est donc plus sûr à cet égard.
EDIT per JasonS:
J'utilise C depuis 1978 et C ++ depuis environ 1987 et j'ai beaucoup d'expérience en utilisant les deux pour les ordinateurs centraux, les mini-ordinateurs et (principalement) les applications intégrées.
Jason fait un commentaire sur l'utilisation de "inline" comme modificateur. (À mon avis, il s'agit d'une capacité relativement "nouvelle" car elle n'existait tout simplement pas pendant la moitié de ma vie ou plus en utilisant C et C ++.) L'utilisation de fonctions en ligne peut en fait effectuer de tels appels (même pour une ligne de code) assez pratique. Et c'est beaucoup mieux, si possible, que d'utiliser une macro en raison du typage que le compilateur peut appliquer.
Mais il y a aussi des limites. La première est que vous ne pouvez pas compter sur le compilateur pour "prendre l'indice". Cela peut ou non. Et il y a de bonnes raisons de ne pas en tenir compte. (Pour un exemple évident, si l'adresse de la fonction est prise, cela nécessite l'instanciation de la fonction et l'utilisation de l'adresse pour passer l'appel nécessitera ... un appel. Le code ne peut pas être inséré alors.) Il y a d'autres raisons aussi. Les compilateurs peuvent avoir une grande variété de critères en fonction desquels ils jugent comment gérer l'indice. Et en tant que programmeur, cela signifie que vous devezpasser du temps à apprendre sur cet aspect du compilateur, sinon vous êtes susceptible de prendre des décisions basées sur des idées erronées. Cela ajoute donc un fardeau à la fois à l'auteur du code et à tout lecteur et à toute personne qui envisage de porter le code sur un autre compilateur.
De plus, les compilateurs C et C ++ prennent en charge la compilation séparée. Cela signifie qu'ils peuvent compiler un morceau de code C ou C ++ sans compiler aucun autre code associé pour le projet. Pour incorporer du code, en supposant que le compilateur puisse choisir de le faire autrement, non seulement il doit avoir la déclaration "in scope" mais il doit aussi avoir la définition. Habituellement, les programmeurs veilleront à ce que ce soit le cas s'ils utilisent «en ligne». Mais il est facile pour les erreurs de s'infiltrer.
En général, bien que j'utilise également inline là où je pense que cela est approprié, j'ai tendance à supposer que je ne peux pas m'y fier. Si la performance est une exigence importante, et je pense que le PO a déjà clairement écrit qu'il y a eu un impact significatif sur les performances lorsqu'ils sont passés à une voie plus "fonctionnelle", alors je choisirais certainement d'éviter de compter sur l'inline comme pratique de codage et suivrait à la place un modèle d'écriture de code légèrement différent, mais entièrement cohérent.
Une note finale sur «en ligne» et les définitions «en portée» pour une étape de compilation séparée. Il est possible (pas toujours fiable) que le travail soit effectué au stade de la liaison. Cela peut se produire si et seulement si un compilateur C / C ++ enfouit suffisamment de détails dans les fichiers objets pour permettre à un éditeur de liens d'agir sur les demandes "en ligne". Personnellement, je n'ai pas connu de système de liaison (en dehors de Microsoft) qui prend en charge cette capacité. Mais cela peut arriver. Encore une fois, la question de savoir si elle doit être invoquée ou non dépendra des circonstances. Mais je suppose généralement que cela n'a pas été pelleté sur l'éditeur de liens, sauf si je le sais autrement sur la base de bonnes preuves. Et si je m'y fie, cela sera documenté à un endroit bien en vue.
C ++
Pour ceux qui sont intéressés, voici un exemple de la raison pour laquelle je reste assez prudent en C ++ lors du codage des applications embarquées, malgré sa disponibilité immédiate aujourd'hui. Je vais jeter quelques termes que je pense que tous les programmeurs C ++ embarqués doivent connaître à froid :
- spécialisation de modèle partielle
- vtables
- objet de base virtuel
- cadre d'activation
- cadre d'activation se détendre
- l'utilisation de pointeurs intelligents dans les constructeurs, et pourquoi
- optimisation de la valeur de retour
Ce n'est qu'une courte liste. Si vous ne savez pas déjà tout sur ces termes et pourquoi je les ai énumérés (et bien d'autres que je n'ai pas énumérés ici), je déconseille l'utilisation de C ++ pour le travail intégré, à moins que ce ne soit pas une option pour le projet .
Jetons un coup d'œil à la sémantique des exceptions C ++ pour obtenir juste une saveur.
UNEB
UNE
.
.
foo ();
String s;
foo ();
.
.
UNE
B
Le compilateur C ++ voit le premier appel à foo () et peut simplement permettre à une séquence d'activation normale de se dérouler si foo () lève une exception. En d'autres termes, le compilateur C ++ sait qu'aucun code supplémentaire n'est nécessaire à ce stade pour prendre en charge le processus de déroulement de trame impliqué dans la gestion des exceptions.
Mais une fois que String s a été créé, le compilateur C ++ sait qu'il doit être correctement détruit avant qu'un déroulement de trame puisse être autorisé, si une exception se produit plus tard. Ainsi, le deuxième appel à foo () est sémantiquement différent du premier. Si le 2ème appel à foo () lève une exception (ce qu'il peut ou non faire), le compilateur doit avoir placé du code conçu pour gérer la destruction de String s avant de laisser se dérouler le cadre habituel. Ceci est différent du code requis pour le premier appel à foo ().
(Il est possible d'ajouter des décorations supplémentaires en C ++ pour limiter ce problème. Mais le fait est que les programmeurs utilisant C ++ doivent simplement être bien plus conscients des implications de chaque ligne de code qu'ils écrivent.)
Contrairement au malloc de C, le nouveau C ++ utilise des exceptions pour signaler quand il ne peut pas effectuer d'allocation de mémoire brute. Il en sera de même pour «dynamic_cast». (Voir 3ème éd. De Stroustrup, Le langage de programmation C ++, pages 384 et 385 pour les exceptions standard en C ++.) Les compilateurs peuvent autoriser ce comportement à être désactivé. Mais en général, vous encourrez des frais généraux en raison des prologues et des épilogues de gestion des exceptions correctement formés dans le code généré, même lorsque les exceptions n'ont pas lieu et même lorsque la fonction en cours de compilation n'a pas de blocs de gestion des exceptions. (Stroustrup l'a déploré publiquement.)
Sans spécialisation partielle des modèles (tous les compilateurs C ++ ne le prennent pas en charge), l'utilisation de modèles peut entraîner un désastre pour la programmation intégrée. Sans cela, la prolifération de code est un risque sérieux qui pourrait tuer un projet intégré de petite mémoire en un éclair.
Lorsqu'une fonction C ++ renvoie un objet, un compilateur temporaire sans nom est créé et détruit. Certains compilateurs C ++ peuvent fournir du code efficace si un constructeur d'objet est utilisé dans l'instruction return, au lieu d'un objet local, réduisant ainsi les besoins de construction et de destruction d'un objet. Mais tous les compilateurs ne le font pas et de nombreux programmeurs C ++ ne sont même pas au courant de cette «optimisation de la valeur de retour».
Fournir un constructeur d'objet avec un seul type de paramètre peut permettre au compilateur C ++ de trouver un chemin de conversion entre deux types de manière complètement inattendue pour le programmeur. Ce type de comportement "intelligent" ne fait pas partie de C.
Une clause catch spécifiant un type de base "découpera" un objet dérivé levé, car l'objet levé est copié en utilisant le "type statique" de la clause catch et non le "type dynamique" de l'objet. Une source non rare de misère d'exception (lorsque vous sentez que vous pouvez même vous permettre des exceptions dans votre code intégré.)
Les compilateurs C ++ peuvent générer automatiquement des constructeurs, des destructeurs, des constructeurs de copie et des opérateurs d'affectation pour vous, avec des résultats inattendus. Il faut du temps pour se familiariser avec les détails de cela.
Le passage de tableaux d'objets dérivés à une fonction acceptant des tableaux d'objets de base génère rarement des avertissements du compilateur mais donne presque toujours un comportement incorrect.
Étant donné que C ++ n'invoque pas le destructeur d'objets partiellement construits lorsqu'une exception se produit dans le constructeur d'objet, la gestion des exceptions dans les constructeurs requiert généralement des "pointeurs intelligents" afin de garantir que les fragments construits dans le constructeur sont correctement détruits si une exception s'y produit. . (Voir Stroustrup, page 367 et 368.) Il s'agit d'un problème courant dans l'écriture de bonnes classes en C ++, mais bien sûr évité en C car C n'a pas la sémantique de construction et de destruction intégrée. Écriture du code approprié pour gérer la construction des sous-objets dans un objet signifie écrire du code qui doit faire face à ce problème sémantique unique en C ++; en d'autres termes "écrire autour" des comportements sémantiques C ++.
C ++ peut copier des objets passés aux paramètres d'objet. Par exemple, dans les fragments suivants, l'appel "rA (x);" peut amener le compilateur C ++ à invoquer un constructeur pour le paramètre p, afin d'appeler ensuite le constructeur de copie pour transférer l'objet x au paramètre p, puis un autre constructeur pour l'objet de retour (un temporaire sans nom) de la fonction rA, qui bien sûr est copié du paramètre p. Pire, si la classe A a ses propres objets qui ont besoin de construction, cela peut se télescoper de façon désastreuse. (Le programmeur AC éviterait la plupart de ces ordures, l'optimisation manuelle étant donné que les programmeurs C n'ont pas une telle syntaxe pratique et doivent exprimer tous les détails un par un.)
class A {...};
A rA (A p) { return p; }
// .....
{ A x; rA(x); }
Enfin, une petite note pour les programmeurs C. longjmp () n'a pas de comportement portable en C ++. (Certains programmeurs C utilisent cela comme une sorte de mécanisme "d'exception".) Certains compilateurs C ++ tentent en fait de régler les choses à nettoyer lorsque le longjmp est utilisé, mais ce comportement n'est pas portable en C ++. Si le compilateur nettoie les objets construits, il n'est pas portable. Si le compilateur ne les nettoie pas, les objets ne sont pas détruits si le code quitte la portée des objets construits à la suite de longjmp et que le comportement n'est pas valide. (Si l'utilisation de longjmp dans foo () ne laisse pas de portée, alors le comportement peut être correct.) Ce n'est pas trop souvent utilisé par les programmeurs intégrés C mais ils doivent se rendre compte de ces problèmes avant de les utiliser.