Commentaires des paramères dans un appel à une fonction

Bonjour,

Le problème que j’ai est récurent, mais je vais partir d’un exemple. Supposons que je vienne d’écrire une fonction pour faire avancer un moteur pas à pas dont le prototype est le suivant:

avance(uint16_t nombreDePas, boolean direction, uint16_t vitesse, uint8_t numeroDuMoteur);

Pour moi, c’est clair, pas besoin de rajouter un commentaire. Mais complètement ailleurs j’appelle cette fonction:

avance(5, 1, 10, 3);

En relisant je ne sais plus ce que veulent dire ces chiffres.
Pour la direction, c’est facile, je définis d’une manière ou d’une autre (define ou struct) le mot SENS_POSITIF. Je peux faire pareil avec les moteurs, je n’en ai qu’un petit nombre. Cela devient:

avance(5, SENS_POSITIF, 10, MOTEUR_3);

Mais je ne peux pas faire pareil pour la vitesse et le nombre de pas.

J’ai essayé:

uint16_t vitesse, nbPas; // Variables poubelles, le contenu n’est jamais utilisé. Définies que pour le nom

et du coup, je peux écrire:

avance(vitesse=5, SENS_POSITIF, nbPas=10, MOTEUR_3);

J’affecte 5 à vitesse mais aussi le résultat au bon paramètre. Je peux aussi écrire

avance(/vitesse=/ 5, SENS_POSITIF, /nbPas=/ 10, MOTEUR_3);

D’un point de vue programmation, c’est bon, mais c’est plus difficile à lire

J’ai imaginé définir vitesse par

#define vitesse 0+
#define nbPas 0+

ce qui permet d’écrire

avance(vitesse 5, SENS_POSITIF, nbPas 10, MOTEUR_3);

Mais il faut absolument qu’il n’y ait pas quelque part une variable locale de même nom

Quelle est la solution pour vitesse et nbPas?

Je répond depuis une tablette.
Possible de faire ça?

#define vitesse(x) x
#define nbPas(x) x
#define MOTEUR(x) x

ce qui permet d’écrire

avance(vitesse(5), SENS_POSITIF, nbPas(10), MOTEUR(3));

Salut , ben c ' est simple , tu met pas de chiffre , mais un nom de variable que tu crees dans ton prog et la variable a le nom que tu veux .

si le proto est :avance(uint16_t nombreDePas, boolean direction, uint16_t vitesse, uint8_t numeroDuMoteur);

uint16_t nombreDePas =5;
bool direction = 1;
uint16_t vitesse;
uint8_t numeroDuMoteur=3;

tu crees les variables correspondantes au proto et tu appelles ta fonction comme ca :

avance( nombreDePas , direction , vitesse , numeroDuMoteur);

tu peux aussi mettre en commentaire directement au dessus de ton appel le prototype dela fonction complet :

// avance(uint16_t nombreDePas, boolean direction, uint16_t vitesse, uint8_t numeroDuMoteur);
avance ( 5 , SENS_POSITIF , 8 , 3);

#define vitesse(x) x
#define nbPas(x) x
#define MOTEUR(x) x

avance(vitesse(5), SENS_POSITIF, nbPas(10), MOTEUR(3));

Ça passe impec, et je peux définir une variable vitesse sans conflits. Ça me plait assez.

tu met pas de chiffre , mais un nom de variable

Quand il y a une variable, pas de problèmes. Mais c'est quand on a des nombres, si il faut affecter les variables, cela va alourdir la lecture.
Il y a beaucoup de fonctions pour lesquelles on passe des valeurs numériques. Pour celles qui sont connues, cela n'apporte souvent pas grand chose, mais les fonctions que l'on définit soit même ou que l'on prend dans une librairie, c'est bien pratique de pouvoir avoir un commentaire.

Voici quelques fonction connues pour lesquelles on passe la plupart du temps des valeurs sans variables:
map, pinMode, digitalWrite, digitalRead, delay, delayMicroseconds, random, Serial.begin
C'est vrai que pour ces fonctions, il n'y a souvent qu'un paramètre, mais quand on cré ou on utilise une bibliothèque, on a souvent plusieurs paramètres.

Les langages modernes ont une syntaxe qui permet de le faire mais ce n’est pas le cas de C ou C++ où il faut recourir à des solutions créatives

Généralement on essaye tant que faire se peut d’éviter ce qu’on appelle des nombres magiques dans le code parce qu’effectivement on ne sait pas ce qu’ils représentent et les conséquences d’en changer un et pas d’autres. Donc on utilise des enum, des const ou constexpr (ou des #define mais on perd le contrôle de cohérence de type du compilateur) qu’on définit en début de programme ou alors qu’on regroupe dans un fichier constantes.h que vous importez partout où vous en avez besoin

S’il y a des dépendances entre constantes qui ne peuvent pas être calculées à la compilation vous pouvez faire des static assert pour que le compilateur vérifie si les règles sont respectées

compilez cela:

const byte value1 = 10;
static_assert(value1 > 15, "Value1 doit être plus grande que 15");

void setup() {
  Serial.begin(115200);
  Serial.println(value1);
}

void loop() {}

va générer une erreur de compilation

[color=orange]exit status 1
static assertion failed: Value1 doit être plus grande que 15[/color]

Mais si vous faites:

const byte value1 = 20;
static_assert(value1 > 15, "Value1 doit être plus grande que 15");

void setup() {
  Serial.begin(115200);
  Serial.println(value1);
}

void loop() {}

ca passe car la contrainte est respectée

Merci pour static_assert, il me semble en avoir déjà entendu parler, mais mon niveau en C++ ne m'avais sans doute pas permis de comprendre.

Il faudra que je jette un œil à constexpr à tête bien reposée.

des #define mais on perd le contrôle de cohérence de type

J'ai travaillé en Pascal ou le contrôle des type était plus sévère. Avec le compilateur associé à l'IDE, j'ai l'impression qu'il permet beaucoup de choses. Si j'ai besoin de

pinMode(led, OUTPUT);

que je définisse led par
#define led 13
uint8_t led=13;
int led=13;
#define led 13
const uint8_t led=13;
const int led=13;
le compilateur fera les conversions tout seul avant de générer le code, ne créera pas de variables, et fera mon pinMode en 40 cycles d'horloge. Il ne contrôle pas si le type demandé par la fonction est le même que celui qui est transmis.

Oui il y a des règles très précises de promotion de type qui sont documentées dans la norme donc

#define ledPin 13
uint8_t ledPin =13;
int ledPin =13;
const uint8_t ledPin =13;
const int ledPin =13;

sont admissibles dans un appel à pinMode() qui attend un uint8_t

Le choix que vous effectuez cependant a un impact sur ce que vous pouvez faire avec ledPin et sur la place mémoire occupée.

Le choix idéal étantconst uint8_t ledPin =13;(enfin autant utiliser LED_BUILTIN dans ce cas :slight_smile: )

Le choix que vous effectuez cependant a un impact sur ce que vous pouvez faire avec ledPin et sur la place mémoire occupée.

Justement pas, avec la ligne
pinMode(led, OUTPUT);
quelle que soit la définition parmi celles que j'ai prises ou avec LED_BUILTIN, cela prends exactement le même temps et exactement le même nombre d'octets en mémoire programme et vive.

Si votre code ne fait rien d’autre le compilateur va faire des optimisations

C'est en général le cas avec pinMode.

Ça m'énerve de voir utiliser un entier pour une fonction qui a besoin d'un octet, mais je ne peux rien dire car cela fonctionne pareil. Mais d'un point de vue beauté du code, cela ne me convient pas.

Non vous êtes juste “vicitime” des optimisations du compilateur.

Quand le code est suffisament simple, le code généré va conserver des valeurs dans des registres ou remplacer directement en binaire les valeurs dans l’arbre d’exécution.

Par exemple si vous compilez cela (j’ai testé sur MEGA)

const uint8_t ledPin = 13;

void setup()
{
  Serial.begin(115200);
  pinMode(ledPin, OUTPUT);

  uint8_t* ptrPin = (uint8_t*) &ledPin;
  Serial.print(F("En mémoire: 0x"));
  for (uint8_t i = 0; i < sizeof(ledPin); i++) {
    if (*ptrPin < 0x10) Serial.write('0');
    Serial.print(*(ptrPin++), HEX);
  }
  Serial.println();
}

void loop() {}

vous verrez que les variables globales prennent 188 octets et 2100 octets de mémoire programme.

Alors que si vous compilez cela

int ledPin = 13;

void setup()
{
  Serial.begin(115200);
  pinMode(ledPin, OUTPUT);

  uint8_t* ptrPin = (uint8_t*) &ledPin;
  Serial.print(F("En mémoire: 0x"));
  for (uint8_t i = 0; i < sizeof(ledPin); i++) {
    if (*ptrPin < 0x10) Serial.write('0');
    Serial.print(*(ptrPin++), HEX);
  }
  Serial.println();
}

void loop() {}

les variables prennent 190 octets et le code 2196 octets.
==> on perd donc 2 octets de SRAM et 96 octets de mémoire flash

si maintenant vous précisez que c’est sur un octet mais ne mettez pas le const

uint8_t ledPin = 13;

void setup()
{
  Serial.begin(115200);
  pinMode(ledPin, OUTPUT);

  uint8_t* ptrPin = (uint8_t*) &ledPin;
  Serial.print(F("En mémoire: 0x"));
  for (uint8_t i = 0; i < sizeof(ledPin); i++) {
    if (*ptrPin < 0x10) Serial.write('0');
    Serial.print(*(ptrPin++), HEX);
  }
  Serial.println();
}

void loop() {}

on aura retrouvé nos 188 octets mais le code fait 2174 octets,

donc on perd 74 octets de mémoire programme

==> dans la vraie vie on gagne à donner le max d’info au compilateur

Dans ce code, le compilateur est obligé d’avoir la variable, sinon il ne pourrait pas créer le pointeur. Dans ce cas, effectivement, il prend 1 ou 2 octets.

Concernant la taille du code, c’est plus compliqué car il doit mettre en place des fonctions supplémentaires qui peuvent être utilisés ailleurs. Je suis en train de jouer avec une fonction d’interruption, et je cherche un peu à optimiser. Mais même si le compilateur n’optimisait pas , le réemploi de fonctions fait que ce n’est pas évident.

Mais je ne comprend pas toujours pourquoi il y a une telle différence dans la taille du code.

Mais si je râle un peu quand je vois
int ledPin = 13;
c’est que ce n’est pas “propre”. Après on finit par voir des
if (bouton==true)…
(en plus ce n’est pas faux et cela prend le même nombre d’octets que sans le true).

Je pense que c’est des reste d’assembleur. J’ai commencé par là.

Concernant la taille du code, c'est plus compliqué car il doit mettre en place des fonctions supplémentaires qui peuvent être utilisés ailleurs

le code est 100% identique à part le type de la variable/constante. Ce que vous voyez ce sont les effets de l'optimiseur de code qui doit dérouler la loop en cas de constante sur 1 octet sans doute, d'où la taille plus petite.

Mais 100% d'accord, ça me fait rager aussi !