LOCODUINO

Discussions Générales => Discussions ouvertes => Discussion démarrée par: Jean-Luc le janvier 11, 2019, 05:05:40 pm

Titre: Programmation : les closures
Posté par: Jean-Luc le janvier 11, 2019, 05:05:40 pm
Bonjour,

J'ai entrepris une petite mise à niveau des nouveautés apportées par C++ 11.

Je ne vais pas parler de tout car c'est quand même très technique mais je voulais vous présenter les « closures » aussi connues sous le nom de « lambda functions » pour plus simplement « fonctions en ligne » qui sont d'une utilité directe et évidente.

Pour illustrer je vais partir d'une exemple avec une Schedule Table (l'article est ici : https://www.locoduino.org/spip.php?article116)
Supposons que nous voulions reproduire le clignotement d'un feu à éclat : un flash bref, disons 50ms, sur une période de 800ms. Avec une Schedule Table, on peut faire comme ceci :

SchedTable<2> flash(800);

void led13On()
{
    digitalWrite(13, HIGH);
}

void led13Off()
{
    digitalWrite(13, LOW);
}

void setup()
{
    flash.at(200, led13On);
    flash.at(250, led13Off);
    pinMode(13, OUTPUT);
    flash.start();
}

void loop()
{
    ScheduleTable::update();
}

Les closures permettent de remplacer la déclaration séparée qui est faite de led13On et led13Off et de les mettre directement dans le programme lors de l'appel de la méthode at, comme ceci :

SchedTable<2> flash(800);

void setup()
{
    flash.at(200, []{ digitalWrite(13, HIGH); } );
    flash.at(250, []{ digitalWrite(13, LOW); } );
    pinMode(13, OUTPUT);
    flash.start();
}

void loop()
{
    ScheduleTable::update();
}

Notez la syntaxe avec les crochets suivis du code entre accolades.

Un autre usage sont les routines d'interruption que l'on attache. Souvent il s'agit juste de mettre à vrai une variable comme par exemple dans cet article : https://www.locoduino.org/spip.php?article148

volatile byte Flag_Recv = 0;   // variable d'échange avec l'interruption IRQ

/*
 *  ISR CAN (Routine de Service d'Interruption)
 *  le flag IRQ monte quand au moins un message est reçu
 *  le flag IRQ ne retombe QUE si tous les messages sont lus
 */
 
void MCP2515_ISR()
{
     Flag_Recv = 1;
}

Et dans setup() :

attachInterrupt(0, MCP2515_ISR, FALLING); // interrupt 0 (pin 2)

Avec les closures on peut écrire directement

attachInterrupt(0, []{ Flag_Recv = 1; }, FALLING); // interrupt 0 (pin 2)
Titre: Re : Programmation : les closures
Posté par: Dominique le janvier 11, 2019, 09:43:38 pm
Merci Jean-Luc,

Je vois que cela permet de ne donner aucun nom a la fonction puisqu'elle est mise d'office dans la fonction qui l'utilise.

Je n'avais encore jamais vu ces crochets [] utilises comme cela.
Encore merci.

Dominique
Titre: Re: Programmation : Range-based for loop
Posté par: bobyAndCo le janvier 12, 2019, 10:50:46 am
Merci Jean-Luc. Ce fil est pour moi une tres bonne initiative car il permet de partager des "petites astuces" de programmation souvent simples a  utiliser et qui nous simplifient les developpements.

Concernant les fonctions lambda, je trouve leur usage particulierement interessant comme parametre de fonction.
La principale difficulte tient a  la lecture du code un peu "deroutante".

Moi qui aime beaucoup utiliser les tableaux, (http://www.locoduino.org/spip.php?article227 (http://www.locoduino.org/spip.php?article227)), je vous propose "Range-based for loop" qui fait aussi partie de la specification C++11. C'est l'equivalent de "for each" qui existe dans d'autres langages (Java, PHP, JavaScript...) mais qui manquait vraiment en C++.

Cette fonction permet de parcourir le contenu d'un tableau (collection) en fonction de ses items et non de ses indexes comme on le connait avec la boucle "for in". Dans le cas de Range-based for loop, il faut comprendre : "for elem in collection

Petit exemple.

/*
   Range-based for loop
   c++11
*/


int v[] = { 1, 4, 3, 8, 2}; // Un tableau d'entiers v

void setup() {
  Serial.begin(115200);

  for (int elem : v) {
    Serial.print(elem);
    Serial.print(" "); // Ecrit : 1 4 3 8 2
  }
}

void loop() {
  // put your main code here, to run repeatedly:

}

C'est la valeur de l'element du tableau qui est ici recuperee directement evitant de passer par son index v[0], v[1]....

Voici un autre exemple qui montre un peu plus l'interet et qui utilise une fonction lambda.

/*
   Range-based for loop
   c++11
*/


int v[] = { 1, 4, 3, 8, 2}; // Un tableau d'entiers v

void setup() {
  Serial.begin(115200);

  for (int elem : v)
    Serial.print([&elem]() { return elem % 2; }()); // Ecrit : 10100

}

void loop() {
  // put your main code here, to run repeatedly:

}

Cette fonction peut par exemple permettre de commuter l'etat d'une broche (0 ou 1) en fonction de valeurs contenues dans un tableau.

Autre exemple, cette fois avec des Strings :

/*
   Range-based for loop
   c++11
*/


String x[] = { "Bonjour ", "Loco", "duino"}; // Un tableau de Strings x

void setup() {
  Serial.begin(115200);

  for (String elem : x)
    Serial.print(elem); // Ecrit : Bonjour Locoduino

}

void loop() {
  // put your main code here, to run repeatedly:

}

Ce meme exemple avec une fonction lambda pour la demonstration mais qui n'apporte rien en terme de programmation (il existe plus simple dans ce cas).

/*
   Range-based for loop
   c++11
*/


String x[] = { "Bonjour ", "Loco", "duino"}; // Un tableau de Strings x

void setup() {
  Serial.begin(115200);

  String str;
  for (String elem : x) { // elem in x
    str += [&elem]() {
      return elem;
    }();
  }
  Serial.print(str); // Ecrit : Bonjour Locoduino

}

void loop() {
  // put your main code here, to run repeatedly:

}

Par contre, on voit mieux l'utilite dans le cas ou l'on chercherait a  commuter l'etat de broches contenues dans un tableau pins[] selon que les valeurs contenues dans un tableau v[] sont paires ou impaires. Ici, la fonction anonyme f() est un objet avec une syntaxe proche de ce que l'on connais en Javascript par exemple.


/*
   Range-based for loop
   c++11
*/


int v[] = { 1, 4, 3, 8, 2}; // Un tableau d'entiers v
int pins[] = { 2, 3, 8, 9, 10 };  // Un tableau de pins

void setup() {
  Serial.begin(115200);

  for (int elem : v) {
    static int i = 0;
    pinMode(pins[i], OUTPUT);
    auto f = [&elem] () {
      return elem % 2;
    };
    digitalWrite(pins[i], f());
    i++;
  }
}

void loop() {
  // put your main code here, to run repeatedly:

}
Titre: Re : Programmation : Le spécificateur de type auto
Posté par: bobyAndCo le janvier 13, 2019, 11:35:15 am
Le mot clef auto qui sert comme spécificateur de type est l’une des nouveautés de C++11 qui ne devrait pas manquer de nous intéresser mais qui interroge également.

C/C++ est un langage à typage fort et tout oubli ou erreur sur ce point est immédiat sanctionné par le compilateur.

Ainsi, si l’on souhaite déclarer et initialiser une variable x avec la valeur 4, il nous faudrait écrire :

int x = 4; // ou char x = 4 ou bien encore, uint8_t x = 4


Dans tous les cas, on indique au compilateur qu’il s’agit d’un type entier. Mais depuis la spécification C++11, il est également possible d’écrire ceci :

auto x = 4; // ou const auto x = 4

ou encore :

const auto pi = 3.14159265358979323846; // au lieu de : const  double pi = 3.14159265358979323846;

Depuis C++14, le spécificateur de type auto peut également être utilisé pour le typage de la valeur retournée par une fonction. Ainsi, vous avez peut-être remarqué dans mon code ci-dessus la chose suivante :

auto f = [&elem] () {
      return elem % 2;
};

Je dis que cela ne devrait pas manquer de nous interroger car, alors que les langages à faible typage ou à typage dynamiques évoluent justement vers un typage fort, C/C++  s’oriente dans la direction inverse qui s'apparente à un typage faible.

Cela est permis grâce à l’usage du compilateur qui dans un grand nombre de cas peut déduire le type d’une variable en fonction de la valeur passée à l’initialisation. Après tout, c’est bien ce qui se passe quand ont utilise #define et que l’on écrit ceci :

#define x 4

Le code ci-dessous est mauvais mais ne présentera d'erreur à la compilation "pi est un entier déterminé lors de son intialisation. Le compilateur n'en fait pas un flottant parce qu'il rencontre pi = 3.14159265358979323846; ensuite". (voir observation de Jean-Luc ci-dessous)

auto x = 4;
auto pi = 3;

void setup() {
  pi = 3.14159265358979323846;
} // Fin Setup

void loop() {
  // put your main code here, to run repeatedly:
}

Le spécificateur de type auto est largement recommandé en programmation C/C++ « classique », mais je ne connais pas assez les spécificités propres aux microcontrôleurs pour savoir si l’usage dans ce cas est aussi recommandé, éventuellement limité, voir banni. La réponse des « experts » dans ce domaine m'intéresserait !




Titre: Re : Programmation : les closures
Posté par: Dominique le janvier 13, 2019, 12:10:29 pm
Ca interroge, tu as raison :

Si, à l'initialisation, la variable de type "int" n'occupe que 2 octets et si, plus tard, elle est amenée à en occuper 4, alors il faut "pousser" de 2 octets toutes les variables suivantes (si ce sont des globales).

Je suppose que, dans ce cas, le compilateur analyse toutes les dimensions d'une variable auto pour lui affecter la plus grande taille.

De même, est-ce qu'un pointeur sur cette variable risque de changer en cours d'exécution ?

Titre: Re : Programmation : les closures
Posté par: Jean-Luc le janvier 13, 2019, 12:59:08 pm
Tout ça, ça reste statique. Donc il n'y a pas d'évolution du type de variable en cours d'exécution (c'est impossible).

Le compilateur, en analysant le code source, détermine le type, si il le peut.

En l'occurrence, le code suivant :

auto a = 4;

void setup() {
  // put your setup code here, to run once:
  a = 3.14159;
  Serial.begin(115200);
  Serial.println(a);
}

void loop() {
  // put your main code here, to run repeatedly:

}

Affiche 3. C'est donc l'initialisation de auto à 4 qui a fixé son type.

Dans l'embarqué, on devrait être préoccupé par le type de données utilisé car la place est comptée et on doit être capable de de dire de quel type il s'agit.

Je limiterais auto à des types non ambigus et pour déclarer des variables locales comme un pointeur de fonction dans l'exemple de closures de Christophe.

Dans l'exemple ci-dessus, il s'avère que a est un entier sur deux octets. Est-il signé ou non signé ? signé si j'en crois ce qui suit :

auto a = 4;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
  Serial.println(sizeof(a));
  a = 3.14159;
  Serial.println(a);
  a = -1;
  Serial.println(a);
  a = 300000;
  Serial.println(a);
}

void loop() {
  // put your main code here, to run repeatedly:
}

Sortie :

2
3
-1
-27680

Il s'avère donc que le compilateur se moque de ce que l'on fait de la variable par la suite, il n'examine que l'initialisation. D'ailleurs si on retire l'initialisation = 4, la compilation est impossible.
Titre: Re: Programmation : Le specificateur de type auto
Posté par: Jean-Luc le janvier 13, 2019, 01:18:38 pm
Alors que ce code à l'inverse est possible parce que le compilateur analyse qu'en cours de programme, le type de pi va changer et applique donc le bon type dès l'initialisation.

auto x = 4;
auto pi = 3;

void setup() {
  pi = 3.14159265358979323846;
} // Fin Setup

void loop() {
  // put your main code here, to run repeatedly:
}

En fait non. pi est un entier déterminé lors de son intialisation. Le compilateur n'en fait pas un flottant parce qu'il rencontre pi = 3.14159265358979323846; ensuite.
Titre: Re?: Programmation : les closures
Posté par: bobyAndCo le janvier 13, 2019, 02:38:56 pm

Je limiterais auto à des types non ambigus et pour déclarer des variables locales comme un pointeur de fonction dans l'exemple de closures de Christophe.


Jean-Luc, dans l'exemple que tu cites, il faut d'ailleurs noter que le type auto est le seul que la compilateur accepte alors que pourtant le modulo renvoye par la fonction est sans ambiguite de type entier :

int pins[] = { 2, 3, 8, 9, 10 };  // Un tableau de pins

void setup() {
  Serial.begin(115200);

  for (int elem : v) {
    static int i = 0;
    pinMode(pins[i], OUTPUT);
    auto f = [&elem] () {
      return elem % 2;
    };
    digitalWrite(pins[i], f());
    i++;
  }
}
Titre: Re : Programmation : les closures
Posté par: Jean-Luc le janvier 13, 2019, 02:51:41 pm
Oui c'est vrai.

Note que le type de f n'est pas un entier, c'est un pointeur de fonction.
Titre: Re�: Programmation : les closures
Posté par: bobyAndCo le janvier 13, 2019, 03:11:53 pm
Oui tu as raison mais on pourrait penser que quelque chose comme ceci pourrait cependant fonctionner : int (*pf)(); -> Mais non !!!
Titre: Re : Programmation : les closures
Posté par: Dominique le janvier 13, 2019, 03:16:17 pm
Humblement, je concluerais qu'il ne vaut mieux pas se servir de "auto", source de confusion, voire de bugs. Non ?


PS : on dirait que les caractères accentués sont maintenant respectés ?
Titre: Re : Programmation : les closures
Posté par: Jean-Luc le janvier 13, 2019, 03:16:47 pm
Non car c'est plus qu'un pointeur de fonction, notamment en ce qui concerne la capture  ;)
Titre: Re : Programmation : les closures
Posté par: bobyAndCo le janvier 13, 2019, 03:25:46 pm
Humblement, je concluerais qu'il ne vaut mieux pas se servir de "auto", source de confusion, voire de bugs. Non ?

Non parce que c'est justement ce que je cherche à montrer ( et Jean Luc explique pourquoi juste au dessus) c'est que dans les closures par exemple, tu n'as pas le choix.

Par ailleurs, cela ne pose aucun problème d'écrire auto x = 4; plutôt que int x = 4;
Titre: Re : Programmation : les closures
Posté par: Jean-Luc le janvier 13, 2019, 03:28:09 pm
Je le réserverais là où c'est utile :

les closures
les range based loop

Pour les types de données simples comme les entiers, ça ajoute de la confusion.
Titre: Re : Re : Programmation : les closures
Posté par: Dominique le janvier 13, 2019, 03:28:37 pm
Par ailleurs, cela ne pose aucun problème d'écrire auto x = 4; plutôt que int x = 4;

Pour la lisibilité du code, je n'en suis pas persuadé, s'il faut que tu cherches ailleurs dans le code pour trouver le type probable.
Titre: Re : Programmation : les closures
Posté par: bobyAndCo le janvier 13, 2019, 03:49:51 pm
Non car c'est plus qu'un pointeur de fonction, notamment en ce qui concerne la capture  ;)

Je suis content que tu parles effectivement de la capture car je pense que l'on ne peut pas bien comprendre et dans tous les cas, pas tirer totalement profit des closures si on n'explique pas la capture et les types de captures.
Titre: Re : Programmation : les closures
Posté par: Thierry le janvier 13, 2019, 08:23:17 pm
Pour l'usage d'un auto, qui s'apparente de très près au 'var' du C#, il a été convenu à mon boulot qu'il était permis uniquement si le type est inscrit en clair sur la ligne, comme un cast ou une allocation par 'new' :

auto c = (int) a;  // forcément un entier...
auto obj = new Toto(); // Forcément un Toto...

les autres écritures qui ne disent par leur type sont interdites :

auto x = 4; // Ne dit rien sur le type...
auto ret = GetValue(); // on ne sait pas ce qu'on récupère.

En C# toujours, l'outil externe Resharper que nous utilisons et qui améliore la gestion du langage et des erreurs est réglé pour signaler comme une erreur tout abus de 'var'. C'est dommage de ne pas pouvoir en faire autant avec Visual ou Gcc.
Titre: Re : Programmation : les closures
Posté par: Jean-Luc le janvier 14, 2019, 08:24:58 am
Mon point de vue est qu’auto à été introduit dans le langage pour éviter des déclarations à la fois verbeuses et redondantes.

Employer auto pour déclarer un entier n’a pas de sens car on perd de l’expressivité sans être moins verbeux.

Donc ça trouve sa place pour allouer dynamiquement un objet :

auto obj = new Truc();

Car obj est a coup sûr un Truc* et c’est moins verbeux que

Truc *obj = new Truc();

Pour les closures comme on l’a déjà dit.

Et écrire :

auto a = (int)c;

C’est un truc de programmeur geek car c’est plus verbeux et moins clair que

int a = c;

 ;D
Titre: Re : Programmation : les closures
Posté par: Thierry le janvier 14, 2019, 08:56:49 am
Effectivement, ça s'applique plutôt dans Accessories sur une déclaration du type :

auto items = new ACCSCHAINEDLIST<GroupStateItem>();
Titre: Re : Programmation : les closures
Posté par: Didier44 le janvier 16, 2019, 05:58:48 pm
Bonjour à tous,

Merci Jean-Luc d'avoir lancé ce post.
J'utilise régulièrement auto dans le boucles, mais il est vrai que pour certaines déclarations dynamiques d'objets , cela peut allèger le code. Je vais m'empresser de le mettre en application.

J'ai découvert les lamda il y a peu et notament dans du code appliqué au serveur avec l'ESP32 ou cela est un peu hermétique au départ mais structure bien le code.
server.on("/hello", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(200, "text/plain", "Hello World");
});