C++ moderne - utilisation simultanée de std::vector et std::string

Bonjour à tous,

Dans le cadre de mes recherches sur le C++, après avoir étudié les classes vector, array, string, le concept de conteneur, d'itérateur, d'algorithmes et j'en passe ; je viens de m'attaquer à l'utilisation des fonctions dites "LAMBDA".
En simplifiant à l'extrême, elles constituent des fonctions potentiellement anonymes destinées à être utilisées pour des opérations locales. Elles permettent d'écrire des prédicats personnalisés dans le cadre de l'utilisation des algorithmes. C'est du moins ce que je comprends à ce stade de mes lectures.
Dans le cadre de mon apprentissage, j'écris du code C++ sous CodeBlocks que je retranscris systématiquement sur l'IDE arduino (carte : DOIT ESP32 DEV KIT V1) en version C++ moderne puis en version classique.

Par exemple dans le cadre de la création d'un programme ayant pour but d'extraire les nombres impairs d'un tableau dynamique, j'utilise l'algorithme find_if prenant pour paramètre une fonction lambda mais au lieu de l'utiliser directement dans cet algorithme, j'en fais une variable :

#include <algorithm>
#include <vector>
using namespace std;
vector<int> const nombres {
  2, 4, 6, 8 , 10, 11 , 13, 16, 17, 19, 21
};


void setup() {
  Serial.begin(115200);
}
void loop() {
  // déclaration itérateur
  auto iterateur {begin(nombres)}; //déclaration de l'itérateur sur le premier élément du tableau nombres
  // déclaration fonction lambda
  auto lambda {[](int nombre) -> bool  {
      return nombre % 2 != 0; // renvoie nombres impairs
    }};
  do
  {
    iterateur = find_if(iterateur, end(nombres), lambda);
    if (iterateur != end(nombres))
    {
      Serial.print( "nombre impair : ");
      Serial.println( *iterateur);
      ++iterateur;
    }

  } while (iterateur != end(nombres));
  Serial.print("\n");
  delay(10000);
}

Dans un code classique, on peut écrire entre autre le code suivant :

int nombres[] = { 2, 4, 6, 8 , 10, 11 , 13, 16, 17, 19, 21};
size_t taille = sizeof nombres / sizeof * nombres;

int  * const ptrDebut = nombres;
int * const ptrFin = nombres + taille;

// itérateur
int *it = ptrDebut;
void setup() {
  Serial.begin(115200);

}

void loop() {
  for (int *iterateur = it; iterateur < ptrFin ; ++iterateur) {
    if (*iterateur  % 2 != 0) {
      Serial.print( "nombre impair : ");
      Serial.println(*iterateur);
    }
  }
  Serial.print("\n");
  delay(10000);
}

Là où ça se corse c'est lorsque je veux utiliser une fonction lambda avec un tableau dynamique et la classe string. Dans ce cas précis le code C++ dit moderne a pour seul but d'imprimer dans le moniteur série un tableau de tableau ou tableau à 2 dimensions. En langage classique ça peut donner ça :

char *const chaines[] {
        "un mot",
        "autre chose",
        "du blabla",
        "du texte",
        "des lettres"
    };

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


}

void loop() {
for (auto &p : chaines) {
  Serial.println(p);
}
delay(10000);
}

En C++ moderne version CodeBlocks ça donne :

#include <algorithm>
#include <iostream>
#include <string>
#include <vector>

using namespace std;

int main()
{
    vector<string> const chaines {
        "un mot",
        "autre chose",
        "du blabla",
        "du texte",
        "des lettres"
    };
    for_each(begin(chaines),end(chaines),
     [](string const & message) -> void
             {
               cout << "message recu : " << message << endl;
             });

    return 0;
}

Par contre sur arduino en version C++ moderne ça plante :

#include <string>
#include <vector>
#include <algorithm>

using namespace std;

vector<string> const chaines {
        "un mot",
        "autre chose",
        "du blabla",
        "du texte",
        "des lettres"
    };

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

}

void loop() {

 for_each(begin(chaines),end(chaines),
     [](string const & message) -> void
             {
               Serial.print(message); 
             });
             
  delay(10000);
}

Message d'erreur :

Arduino : 1.8.19 (Windows 10), Carte : "DOIT ESP32 DEVKIT V1, 80MHz, 921600, None, Disabled"

D:\Utilisateur\Documents\Arduino\CPP_Lambda_1\CPP_Lambda_1.ino: In lambda function:

CPP_Lambda_1:25:36: error: no matching function for call to 'HardwareSerial::print(const string&)'

                Serial.print(message);

                                    ^

In file included from C:\Users\Utilisateur\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.5\cores\esp32/Stream.h:26,

                 from C:\Users\Utilisateur\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.5\cores\esp32/Arduino.h:167,

                 from sketch\CPP_Lambda_1.ino.cpp:1:

C:\Users\Utilisateur\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.5\cores\esp32/Print.h:81:12: note: candidate: 'size_t Print::print(const __FlashStringHelper*)'

     size_t print(const __FlashStringHelper *);

            ^~~~~

C:\Users\Utilisateur\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.5\cores\esp32/Print.h:81:12: note:   no known conversion for argument 1 from 'const string' {aka 'const std::__cxx11::basic_string<char>'} to 'const __FlashStringHelper*'

C:\Users\Utilisateur\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.5\cores\esp32/Print.h:82:12: note: candidate: 'size_t Print::print(const String&)'

     size_t print(const String &);

            ^~~~~

C:\Users\Utilisateur\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.5\cores\esp32/Print.h:82:12: note:   no known conversion for argument 1 from 'const string' {aka 'const std::__cxx11::basic_string<char>'} to 'const String&'

C:\Users\Utilisateur\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.5\cores\esp32/Print.h:83:12: note: candidate: 'size_t Print::print(const char*)'

     size_t print(const char[]);

            ^~~~~

C:\Users\Utilisateur\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.5\cores\esp32/Print.h:83:12: note:   no known conversion for argument 1 from 'const string' {aka 'const std::__cxx11::basic_string<char>'} to 'const char*'

C:\Users\Utilisateur\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.5\cores\esp32/Print.h:84:12: note: candidate: 'size_t Print::print(char)'

     size_t print(char);

            ^~~~~

C:\Users\Utilisateur\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.5\cores\esp32/Print.h:84:12: note:   no known conversion for argument 1 from 'const string' {aka 'const std::__cxx11::basic_string<char>'} to 'char'

C:\Users\Utilisateur\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.5\cores\esp32/Print.h:85:12: note: candidate: 'size_t Print::print(unsigned char, int)'

     size_t print(unsigned char, int = DEC);

            ^~~~~

C:\Users\Utilisateur\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.5\cores\esp32/Print.h:85:12: note:   no known conversion for argument 1 from 'const string' {aka 'const std::__cxx11::basic_string<char>'} to 'unsigned char'

C:\Users\Utilisateur\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.5\cores\esp32/Print.h:86:12: note: candidate: 'size_t Print::print(int, int)'

     size_t print(int, int = DEC);

            ^~~~~

C:\Users\Utilisateur\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.5\cores\esp32/Print.h:86:12: note:   no known conversion for argument 1 from 'const string' {aka 'const std::__cxx11::basic_string<char>'} to 'int'

C:\Users\Utilisateur\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.5\cores\esp32/Print.h:87:12: note: candidate: 'size_t Print::print(unsigned int, int)'

     size_t print(unsigned int, int = DEC);

            ^~~~~

C:\Users\Utilisateur\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.5\cores\esp32/Print.h:87:12: note:   no known conversion for argument 1 from 'const string' {aka 'const std::__cxx11::basic_string<char>'} to 'unsigned int'

C:\Users\Utilisateur\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.5\cores\esp32/Print.h:88:12: note: candidate: 'size_t Print::print(long int, int)'

     size_t print(long, int = DEC);

            ^~~~~

C:\Users\Utilisateur\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.5\cores\esp32/Print.h:88:12: note:   no known conversion for argument 1 from 'const string' {aka 'const std::__cxx11::basic_string<char>'} to 'long int'

C:\Users\Utilisateur\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.5\cores\esp32/Print.h:89:12: note: candidate: 'size_t Print::print(long unsigned int, int)'

     size_t print(unsigned long, int = DEC);

            ^~~~~

C:\Users\Utilisateur\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.5\cores\esp32/Print.h:89:12: note:   no known conversion for argument 1 from 'const string' {aka 'const std::__cxx11::basic_string<char>'} to 'long unsigned int'

C:\Users\Utilisateur\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.5\cores\esp32/Print.h:90:12: note: candidate: 'size_t Print::print(long long int, int)'

     size_t print(long long, int = DEC);

            ^~~~~

C:\Users\Utilisateur\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.5\cores\esp32/Print.h:90:12: note:   no known conversion for argument 1 from 'const string' {aka 'const std::__cxx11::basic_string<char>'} to 'long long int'

C:\Users\Utilisateur\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.5\cores\esp32/Print.h:91:12: note: candidate: 'size_t Print::print(long long unsigned int, int)'

     size_t print(unsigned long long, int = DEC);

            ^~~~~

C:\Users\Utilisateur\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.5\cores\esp32/Print.h:91:12: note:   no known conversion for argument 1 from 'const string' {aka 'const std::__cxx11::basic_string<char>'} to 'long long unsigned int'

C:\Users\Utilisateur\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.5\cores\esp32/Print.h:92:12: note: candidate: 'size_t Print::print(double, int)'

     size_t print(double, int = 2);

            ^~~~~

C:\Users\Utilisateur\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.5\cores\esp32/Print.h:92:12: note:   no known conversion for argument 1 from 'const string' {aka 'const std::__cxx11::basic_string<char>'} to 'double'

C:\Users\Utilisateur\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.5\cores\esp32/Print.h:93:12: note: candidate: 'size_t Print::print(const Printable&)'

     size_t print(const Printable&);

            ^~~~~

C:\Users\Utilisateur\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.5\cores\esp32/Print.h:93:12: note:   no known conversion for argument 1 from 'const string' {aka 'const std::__cxx11::basic_string<char>'} to 'const Printable&'

C:\Users\Utilisateur\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.5\cores\esp32/Print.h:94:12: note: candidate: 'size_t Print::print(tm*, const char*)'

     size_t print(struct tm * timeinfo, const char * format = NULL);

            ^~~~~

C:\Users\Utilisateur\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.5\cores\esp32/Print.h:94:12: note:   no known conversion for argument 1 from 'const string' {aka 'const std::__cxx11::basic_string<char>'} to 'tm*'

exit status 1

no matching function for call to 'HardwareSerial::print(const string&)'

Voilà, je me demande si l'utilisation de la classe <string> avec la classe <vector> n'est pas à l'origine de ce plantage ?

PS : Comprenez bien qu'il ne s'agit pas pour moi de vous montrer ma science. Non, vous voyez bien sûr qu'elle est encore énormément limitée ! J'ai volontairement donné tous les détails car ça me permet de conceptualiser tout ce que j'apprends d'autant plus que le passage du C++ format CodeBlocks au C++ moderne et classique format arduino est une gymnastique intellectuelle qui me déstabilise lourdement mais que je m'impose. J'ai besoin d'en passer par là pour synthétiser toutes ces informations !

Merci par avance.

Le message d’erreur et dû au fait que la classe Print ne connaît pas std::string

Faites Serial.print(message.c_str());

———
Au lieu de faire deux fois le test de fin

On pourrait faire


auto iterateur = find_if(begin(nombres), end(nombres), lambda);

while (iterateur != end(nombres)) {
      Serial.print( "nombre impair : ");
      Serial.println( *iterateur);
      iterateur = find_if(iterateur, end(nombres), lambda);
}

C'est bien le problème et la solution.

C'est bien plus simple comme ça effectivement :

 auto iterateur = find_if(begin(nombres), end(nombres), lambda);

  while (iterateur != end(nombres)) {
    Serial.print( "nombre impair : ");
    Serial.println( *iterateur);
    iterateur = find_if(iterateur, end(nombres), lambda);
     ++iterateur;
  }

Merci beaucoup.
Bonne soirée.

Le ++ doit être avant sans doute d’ailleurs


while (iterateur != end(nombres)) {
    Serial.print( "nombre impair : ");
    Serial.println( *iterateur);
    iterateur = find_if(++iterateur, end(nombres), lambda);
  }

Bonsoir @J-M-L,
C'est encore bien mieux (en d'autres circonstances vous m'en aviez déjà fait la remarque). Dans les livres, ils préfèrent décomposer les instructions si je peux m'exprimer ainsi ! Du coup comme je lis beaucoup, j'oublie ce que vous m'apprenez et c'est une faute de ma part !

Quant à cette remarque :

Si on reste sous ce format :

ou

 while (iterateur != end(nombres)) {
    Serial.print( "nombre impair : ");
    Serial.println( *iterateur);
    iterateur = find_if(iterateur, end(nombres), lambda);
     iterateur++;
  }

sont équivalents : la Pré-incrémentation ou la Post-incrémentation constituent les mêmes instructions et aboutissent au même résultat.

Par contre dans ce format, la Pré-incrémentation est la condition sine qua non à la bonne exécution des instructions

En effet, la règle est la suivante :
1/ Pré-incrémentation :

int j, k=6;
j = ++k;

équivalent à k = k + 1 ; j = k;

2/ Post-incrémentation :

int j, k=6;
j = k++;

équivalent à j = k ; k = k + 1 ;

3/ Si l'opérateur est uniquement employé pour modifier son opérande :
Ces 3 lignes de code sont équivalentes

++k,
k++;
k = k + 1 ;

Avec votre dernier format on se trouve dans le cas 1 :
iterateur s'incrémente d'abord puis iterateur = find_if(++iterateur, end(nombres), lambda); s'exécute.
Si on effectue une Post-incrémentation, il n'y a pas d'itération !!
donc oui, le ++ doit être avant sans équivoque.

Merci encore.
Bonne soirée

Ce que je voulais dire c’est qu’il faut faire

iterateur++;
iterateur = find_if(iterateur, end(nombres), lambda);

Dans cet ordre, pas faire le ++ après le find

Bonjour @J-M-L ,
Oui effectivement iterateur++; ne doit pas être placé après le find ni avant le Serial.println( *iterateur); (bien évidement pour ce dernier cas de figure).

Seules les itérations suivantes sont correctes :

iterateur = find_if(++iterateur, end(nombres), lambda);
++iterateur;
 iterateur = find_if(iterateur, end(nombres), lambda);
iterateur++;
 iterateur = find_if(iterateur, end(nombres), lambda);

J'en déduit que la déclaration :

auto iterateur = find_if(begin(nombres), end(nombres), lambda);

traite déjà la première adresse ?

Merci.

find_if vous trouve le premier iterateur (élément dans la collection) pour lequel la condition donnée par la fonction lambda est vraie donc oui si on rentre dans le while c’est déjà avec un élément qui correspond

Comme la recherche se fait à partir du premier élément fourni, si on ne passe pas au suivant avant d’appeler find_if on va rester sur place

Merci @J-M-L,
J'ai bien compris.

Bonne journée.

Je reviens vers vous @J-M-L concernant les fonction lambdas.

Il semble que notre compilateur interdise les paramètres génériques :
En effet ce code ne compile pas :

#include <string>
using namespace std;
void setup() {
  Serial.begin(115200);

}

void loop() {
  auto lambda =  [](auto const & parametre) -> void
  {
    Serial.print( "message reçu : ")  ;
    Serial.println( parametre);
  };

  lambda(405);

 
  delay(10000);

}

erreur :

use of 'auto' in lambda parameter declaration only available with -std=c++14 or -std=gnu++14

alors que celui-ci fonctionne parfaitement :

#include <string>
using namespace std;
void setup() {
  Serial.begin(115200);

}

void loop() {

  auto lambda =  [](int  const & parametre) -> void
    {
     
      Serial.print( "message reçu : ")  ;
      Serial.println( parametre);
    };
  
    lambda(405);
  delay(10000);

}

D'ailleurs ça semble cohérent car si j'utilisais l'instruction lambda("string"); avec un paramètre générique, je serai de toute façon obligé de faire un Serial.println( parametre.c_str()); donc de préciser en quelque sorte le type à l'impression ce qui rend inapproprié l'utilisation des paramètres génériques.

C'est deux chose différentes, je pense, la fonction println de classe Serial ne connait pas le type de ton objet.
Donc on utilise une fonction de cette objet pour lui fournir un type qu'il connait.
Le compilateur n'intervient dans ce cas là.

Je ne sais pas quels sont les raisons pour laquelle l'IDE n'utilise pas les options de compilation c++14, mais on pourrais imaginer que cela soit possible si la raison n'est pas bloquante.

Bonjour @terwal,

C'est tout à fait exact. La seule chose que je veux dire c'est qui si toutefois nous avions la possibilité d'utiliser les paramètres génériques, nous tomberions sur une incohérence :
Les paramètres génériques permettent de se passer du type mais à quoi bon si notre fonction print ne reconnait pas le type string (string étant une classe largement utilisée).

J'ai du encore une fois mal m'exprimer.

Bonne journée.

l'usage d'auto dans une lambda nécessite en effet une version plus élevée du compilateur et notre compilateur par défaut (celui de l'IDE) c'est C++11. Arduino n'a pas qualifié l'ensemble de la toolchain et de ses bibliothèques sous C++14 pour le moment (ni sur C++17)

ça fait partie des demandes, cf Enable C++14 support by default by federicobond · Pull Request #307 · arduino/ArduinoCore-avr · GitHub

Il est possible de rajouter d'autres types de paramètres supportés par la fonction print, la bibliothèque pourrait être modifiée.

si vous jouez avec du C++ un peu avancé il faut de toutes façons oublier les AVR et avrlibc et leur peu de mémoire. Un proc 32 bits avec un peu plus de mémoire permet de supporter plus de choses

Merci @J-M-L pour ces précisions. Le C++ moderne simplifie énormément la manière de programmer. Un peu trop à mon sens, aujourd'hui on a plus besoin de déclarer le type d'une variable, demain les types disparaîtront un peu comme les fonctions lambdas sans type défini. Dans quelques années les apprentis ingénieurs programmeront avec des outils de création et de modification graphique, un peu comme avec Flowgorithm.
Je ne regrette pas d'être passé par le C et d'en avoir acquis les bases essentiellement grâce à vous.

Il me semble avoir compris que dans la filière normale de l'enseignement, les mathématiques étaient devenues une option mais que cela allait changer à nouveau (je ne suis pas sûr). Que dire du Français quand je vois comment mes petits-enfants écrivent et s'expriment ! Je sais, je fais des fautes d'orthographe mais eux c'est de la folie et ce n'est pas de leur faute, c'est la faute du système ... On pourrait parler du fait que le Ministère de l'Education Nationale , pour faciliter sa gestion administrative des postes a soudainement décrété une polyvalence en 2008...
Bref, un jour les @J-M-L, @dfgh, @lesept, @biggil, @68tjs , @al1fch, @hbachetti et tous les autres seront remplacés par des experts en programmation graphique :smiley:
Bon je sais, je vais trop loin mais quand même. A moins que je ne sois devenu un vieux con :grinning:
Bon voilà c'était mon coup de gueule !
Bonne soirée à tous.

1 Like

attention à la compréhension qui serait que auto est un type générique magique en C++

En fait auto ça dit juste au compilateur de déduire automatiquement le type voulu en fonction du contexte, par exemple (minimum C++14) pour le type de retour d'une fonction

auto f() {
  return 42;
}

comme 42 est un int, le compilateur va savoir que la fonction retourne un int et va remplacer auto par int
c'est pour cela que auto est appelé un Placeholder type specifiers

donc si le contexte ne dit pas ce que l'on veut, ça ne fonctionnera pas. Il faudra toujours les types.


surtout pour certains ici comme moi, un retraité est facilement remplacé ou cède sa place "naturellement" :slight_smile:

la tendance actuelle du "no code" c'est gentil mais il faut des gens qui savent coder pour que ceux qui ne savent pas puisse faire des petits projets

Oui @J-M-L, j'exagère mais en quelque sorte la déclaration des types est devenue implicite car elle peut être déduite par le contexte. On a plus besoin de faire une déclaration explicite. Du moins c'est ce que je comprends.
D'ailleurs la fonction lambda n'a pas de type, c'est le compilateur qui le définit.

La programmation graphique était la tendance il y longtemps, mais en dehors de pouvoir coder visuellement une interface graphique, ce genre de technique a vite montré ses limites.
Actuellement, dans l'industrie, on est revenu au code pur et dur, et je dirais même que les IDE évolués ont cédé la place aux éditeurs classiques. Aucun développeur professionnel n'accepte de perdre son temps avec un outil qui tombe en rade sans raisons du jour au lendemain, après une mise à jour par exemple.

Par contre, de nouvelles manières de travailler sont apparues : développement agile, intégration continue, tests unitaires et d'intégration automatisés, etc.

Certains langages ont déjà actuellement des variable avec des types dynamiques et cela n'est pas récent.
Il est très difficile de savoir ce que l'avenir nous réserve pour les développeurs, le deep leanrning, les machines quantiques ne sont encore qu'a leur début pour la création de logiciel

Bonjour @hbachetti,
vous me rassurez car j'ai cru comprendre qu'en école préparatoire d'ingénieur on utilise Flowgorithm dixit @ahocke.

C'est plus compliqué que cela dans ce cas.

En C++11, en fait une lambda n'a pas de type habituel (on dit que c'est de type "closure"), quand vous introduisez une lambda vous créez un nouveau type de classe mais qui n'a pas de nom :slight_smile: et qui surcharge l'opérateur () (appel de fonction)

Vous pouvez voir la partie capture (entre les []) comme les paramètres du constructeur et la partie entre parenthèse devient les paramètres de la fonction.

si vous voulez plus de joyeusetés lisez aussi des trucs sur les functor et std::function