Auteur Sujet: Programmation : les closures  (Lu 20751 fois)

Jean-Luc

  • Global Moderator
  • Hero Member
  • *****
  • Messages: 1691
    • Voir le profil
Programmation : les closures
« 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)
Cordialement

Dominique

  • Global Moderator
  • Hero Member
  • *****
  • Messages: 2898
  • 100% Arduino et N
    • Voir le profil
Re : Programmation : les closures
« Réponse #1 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
Cordialement,
Dominique

bobyAndCo

  • Global Moderator
  • Hero Member
  • *****
  • Messages: 914
  • HO avec DCC++
    • Voir le profil
Re: Programmation : Range-based for loop
« Réponse #2 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), 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:

}
« Modifié: janvier 12, 2019, 09:33:52 pm par bobyAndCo »

bobyAndCo

  • Global Moderator
  • Hero Member
  • *****
  • Messages: 914
  • HO avec DCC++
    • Voir le profil
Re : Programmation : Le spécificateur de type auto
« Réponse #3 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 !




« Modifié: janvier 13, 2019, 02:25:54 pm par bobyAndCo »

Dominique

  • Global Moderator
  • Hero Member
  • *****
  • Messages: 2898
  • 100% Arduino et N
    • Voir le profil
Re : Programmation : les closures
« Réponse #4 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 ?

Cordialement,
Dominique

Jean-Luc

  • Global Moderator
  • Hero Member
  • *****
  • Messages: 1691
    • Voir le profil
Re : Programmation : les closures
« Réponse #5 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.
« Modifié: janvier 13, 2019, 01:10:22 pm par Jean-Luc »
Cordialement

Jean-Luc

  • Global Moderator
  • Hero Member
  • *****
  • Messages: 1691
    • Voir le profil
Re: Programmation : Le specificateur de type auto
« Réponse #6 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.
« Modifié: janvier 13, 2019, 01:20:34 pm par Jean-Luc »
Cordialement

bobyAndCo

  • Global Moderator
  • Hero Member
  • *****
  • Messages: 914
  • HO avec DCC++
    • Voir le profil
Re?: Programmation : les closures
« Réponse #7 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++;
  }
}
« Modifié: janvier 13, 2019, 02:41:48 pm par bobyAndCo »

Jean-Luc

  • Global Moderator
  • Hero Member
  • *****
  • Messages: 1691
    • Voir le profil
Re : Programmation : les closures
« Réponse #8 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.
Cordialement

bobyAndCo

  • Global Moderator
  • Hero Member
  • *****
  • Messages: 914
  • HO avec DCC++
    • Voir le profil
Re�: Programmation : les closures
« Réponse #9 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 !!!

Dominique

  • Global Moderator
  • Hero Member
  • *****
  • Messages: 2898
  • 100% Arduino et N
    • Voir le profil
Re : Programmation : les closures
« Réponse #10 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 ?
Cordialement,
Dominique

Jean-Luc

  • Global Moderator
  • Hero Member
  • *****
  • Messages: 1691
    • Voir le profil
Re : Programmation : les closures
« Réponse #11 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  ;)
Cordialement

bobyAndCo

  • Global Moderator
  • Hero Member
  • *****
  • Messages: 914
  • HO avec DCC++
    • Voir le profil
Re : Programmation : les closures
« Réponse #12 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;

Jean-Luc

  • Global Moderator
  • Hero Member
  • *****
  • Messages: 1691
    • Voir le profil
Re : Programmation : les closures
« Réponse #13 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.
Cordialement

Dominique

  • Global Moderator
  • Hero Member
  • *****
  • Messages: 2898
  • 100% Arduino et N
    • Voir le profil
Re : Re : Programmation : les closures
« Réponse #14 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.
Cordialement,
Dominique