IA une façon bien originale d'utiliser l'opérateur ternaire

Bonjour,
Je tenais à partager une information obtenue auprès de l'IA :

Je viens de découvrir que l'opérateur ternaire peut être utilisé d'une manière bien originale (pour moi en tout cas) :

1/ J'avais mis dans mon code un switch - case :

switch(type) {
    case GPRMC: return parseRMC(champ);
    case GPGGA: return parseGGA(champ);
    case GPGSA: return parseGSA(champ);
    default:    return false;
  }

2/ à la place je mets un opérateur ternaire :

return  (type == GPRMC) ? parseRMC(champ) :            // 3ᵉ opérande ↓
         (type == GPGGA) ? parseGGA(champ) :            // 3ᵉ opérande ↓
         (type == GPGSA) ? parseGSA(champ) :            // 3ᵉ opérande ↓
         false;                       // dernier « faux »

L'opérateur ternaire c'est :
condition ? expression_si_vrai : expression_si_faux

--> Dans la cascade qui est utilisée, l’expression-si-faux de chaque ternaire est… un autre ternaire, et ainsi de suite, jusqu’au dernier où l’on met false.
Donc chacun des ?: possède bien ses trois parties. Si on supprimait le tout dernier : false, le ternaire précédent manquerait de 3ᵉ opérande et le compilateur lèverait une erreur.

Voilà ça fonctionne parfaitement et j'ai trouvé l'information tellement originale que j'ai souhaité en faire un retour.

Bonne journée.

Bonjour @philippe86220
En effet, c'est original :slight_smile: Mais quel est l’intérêt ? N'est-ce pas plus simple et lisible de faire le tout avec un switch case ? (Je pose la question, mon niveau ne me permettant pas d'en juger :wink:)

C'est l'IA qui vous a proposé spontanément de changer de structure conditionnelle ?

Cordialement,
Pandaroux007

Bonjour @pandaroux007

Non c'est moi qui l'est amenée sur ce terrain.

Ta question c'est un peu : quel est l'interêt de remplacer un switch - case par un if - else. c'est possible tout simplement mais en général et c'est moi qui parle, il me semble qu'on mettra plus un switch - case si il y a beaucoup plus de conditions (vu dans mes livres).
Pour l'opérateur ternaire c'est pour moi si surprenant mais maintenant si logique que je tenais à partager cette information.

Après certains pourront évoquer la lisibilité mais il me semble qu'elle est acquise lorsque comprise : ce que je comprends est pour moi lisible. Bon après personne ne passera derrière moi.

Enfin ça fait partie d'une pseudo culture générale de la programmation, il faut peut-être le voir comme ça :wink:

Bonne journée.

PS : après je laisse les plus compétents que moi donner leur avis si ils le souhaitent.

Ce n'était pas exactement ma question, mais ce n'est pas grave car vous y avez répondu ici

Si je résume :

  • le if/else, c'est pour quelques conditions,
  • le switch/case, c'est quand on a pléthore de conditions,
  • et l'opérateur ternaire, c'est quand on a un si/sinon que l'on veut simplifier en le passant sur une ligne - même s'il est possible d'imbriquer l'opérateur ça reste plus une possibilité étonnante du C++ qu'une fonctionnalité à utiliser.

C'est ça ?

Voici un prompt que je viens de soumettre à l'IA :

entre un switch case, un if else et un opérateur ternaire, est-ce que cela change vraiment quelque chose ?

Réponses de ChatGPT :

Même résultat fonctionnel … mais pas forcément les mêmes coûts ni la même lisibilité

Sur un ATmega328P (Arduino Uno) les trois formes se traduisent toutes par des branchements conditionnels, mais le compilateur choisit des stratégies différentes :

Forme source Comment le compilateur s’y prend Impact vitesse Impact flash Lisibilité/maintenabilité
switch-case décimal serré → table de sauts ; éclaté → chaîne de cmp/branch • Table : un seul saut indirect (:high_voltage: plus rapide pour ≥5 cases contiguës)
• Chaîne : comme un if • Table : +4 à 6 oct. par case
• Chaîne : pareil qu’if ★★★ clair pour de nombreuses branches, syntaxe extensible
if … else if … suite de comparaisons et sauts directs O(n) tests ; OK si n faible +2 octets par test env. ★★ moyen ; devient lourd quand ça s’allonge
ternaire ?: identique à une chaîne d’ifmais toujours une expression identique à l’if identique à l’if ★ compact pour 1-2 cas ; illisible au-delà

Exemple concret (enum serré) :

enum Msg { GPRMC, GPGGA, GPGSA, AUTRE };

switch (type) {
  case GPRMC: actionRMC(); break;
  case GPGGA: actionGGA(); break;
  case GPGSA: actionGSA(); break;
  default:    erreur();
}
  • GCC‐AVR génère une table de sauts :
    1 comparaison, puis ijmp → chaque case atteint en 2 cycles ≈.
  • Avec l’équivalent en if : 2 comparaisons sur la 3ᵉ branche → un peu plus lent ; code objet quasi identique au ternaire.

Quand choisir quoi ?

Choisissez Quand
switch • Enum ou plage de valeurs denses (0-4, 10-13…)
• > 3 branches
• Actions distinctes mais simples
if … else • Tests dispersés ou complexes (val > 123 && flag)
• Une ou deux branches ; besoin de variables locales par branche
Ternaire • Vous avez besoin d’une valeur unique in-line (initialisation, argument, return)
• Cas 2-3 max, courts

Sur un microcontrôleur 8 bit comme le 328P

  • Différences de vitesse : mesurables (switch table plus rapide) mais rarement critiques pour du décodage NMEA.
  • Différences de flash : une table peut coûter quelques dizaines d’octets ; acceptable si vous êtes encore très loin de la limite (vous l’êtes !).
  • Clarté : prime dans 99 % des cas — code relu, code maintenu.

TL;DR

Fonctionnellement identiques ; performance ≈ égale sauf switch + table légèrement plus rapide quand les valeurs sont serrées.
Choisissez donc :

  • switch dès 3-4 branches d’un enum propre,
  • if … else pour des conditions variées,
  • le ternaire pour une affectation/retour ultra-court.
    -------------FIN REPONSE DE CHATGPT-----------

Après moi je ne suis pas un champion, je sais seulement que les 3 façons de faire fonctionnent et c'est ici le partage qui m'a motivé. Après c'est sûr que pour une vingtaine de conditions je ne fais pas le choix du ternaire .... question de logique :wink:

1 Like

@pandaroux007 Perso, je pense qu'il faut privilégier la lisibilité dans la programmation d'un micro-controleur. La vitesse d'exécution est rarement critique par rapport à certains logiciels développé ssur ordinateur.

Bonjour axelmaux

Je plussoie, surtout si le programme est publié dans un forum comme celui-ci.

Cordialement
jpbbricole

1 Like

Bêtement, je fais un trois tests:
− le premier avec un switch
− le deuxième avec un if
− le troisième avec un opérateur ternaire:

Les trois programmes de tests:
Avec switch:

volatile uint8_t N, resultat; // Pour pouvoir faire les tests, volatile pour éviter les optimisations
volatile long temps, // Mesure du temps avec millis() 
              boucle; // Nombre de boucles pour avoir une certaine précision

void setup()
{
  Serial.begin(115200); // Initialisation de la console
}

void loop()
{
  Serial.print("Avec switch: ");
  N = random(7);
  temps = millis(); // Début de la mesure
  for (boucle=0; boucle<800000L; boucle++) switch (N)
  {
    case 0: resultat += 4; break;
    case 1: resultat += 6; break;
    case 2: resultat += 9; break;
    case 3: resultat += 8; break;
    case 4: resultat += 2; break;
    case 5: resultat += 7; break;
    case 6: resultat += 1; break;
    case 7: resultat += 8; break;
  }
  // Calcul avec "resultat" (qui vaut forcément 0) pour éviter un code inutile qui peut être supprimé sans avertissement 
  Serial.print(millis() - temps + resultat); // Fin de la mesure
  Serial.println("ms");
}

Avec if:

volatile uint8_t N, resultat; // Pour pouvoir faire les tests, volatile pour éviter les optimisations
volatile long temps, // Mesure du temps avec millis() 
              boucle; // Nombre de boucles pour avoir une certaine précision

void setup()
{
  Serial.begin(115200); // Initialisation de la console
}

void loop()
{
  Serial.print("Avec if: ");
  N = random(7);
  temps = millis(); // Début de la mesure
  for (boucle=0; boucle<800000L; boucle++) if (N < 4)
  {{ // N = 0 à 3
    if (N < 2)
    {{ // N = 0 ou 1
      if (N==0) resultat += 4; else resultat += 6;
    }}
    else
    {{ // N = 2 ou 3
      if (N==2) resultat += 9; else resultat += 8;
    }}
  }}
  else
  {{ // N = 4 à 7
    if (N < 6)
    {{ // N = 4 ou 5
      if (N==4) resultat += 2; else resultat += 7;
    }}
    else
    {{ // N = 6 ou 7
      if (N==6) resultat += 1; else resultat += 8;
    }}
  }}
  // Calcul avec "resultat" (qui vaut forcément 0) pour éviter un code inutile qui peut être supprimé sans avertissement 
  Serial.print(millis() - temps + resultat); // Fin de la mesure
  Serial.println("ms");
}

Avec l'opérateur ternaire:

volatile uint8_t N, resultat; // Pour pouvoir faire les tests, volatile pour éviter les optimisations
volatile long temps, // Mesure du temps avec millis() 
              boucle; // Nombre de boucles pour avoir une certaine précision

void setup()
{
  Serial.begin(115200); // Initialisation de la console
}

void loop()
{
  Serial.print("Avec ?:; : ");
  N = random(7);
  temps = millis(); // Début de la mesure
  for (boucle=0; boucle<800000L; boucle++) (N < 4)?
  // N = 0 à 3
    (N < 2)?
    // N = 0 ou 1
      (N==0)? resultat += 4: resultat += 6:
    // N = 2 ou 3
      (N==2)? resultat += 9: resultat += 8:
  // N = 4 à 7
    (N < 6)?
      // N = 4 ou 5
      (N==4)? resultat += 2: resultat += 7:
      // N = 6 ou 7
      (N==6)? resultat += 1: resultat += 8;
  
  // Calcul avec "resultat" (qui vaut forcément 0) pour éviter un code inutile qui peut être supprimé sans avertissement 
  Serial.print(millis() - temps + resultat); // Fin de la mesure
  Serial.println("ms");
}

Le résultat avec une Uno et switch:

Le croquis utilise 2398 octets (7%) de l'espace de stockage de programmes. Le maximum est de 32256 octets.
Les variables globales utilisent 218 octets (10%) de mémoire dynamique, ce qui laisse 1830 octets pour les variables locales. Le maximum est de 2048 octets.
Avec switch: 3067ms
Avec switch: 3069ms
Avec switch: 3170ms
Avec switch: 3169ms
Avec switch: 3169ms
Avec switch: 3169ms
Avec switch: 3169ms
Avec switch: 3168ms

Le résultat avec une Uno et if:

Le croquis utilise 2442 octets (7%) de l'espace de stockage de programmes. Le maximum est de 32256 octets.
Les variables globales utilisent 214 octets (10%) de mémoire dynamique, ce qui laisse 1834 octets pour les variables locales. Le maximum est de 2048 octets.
Avec if: 2665ms
Avec if: 2666ms
Avec if: 2817ms
Avec if: 2868ms
Avec if: 2867ms
Avec if: 2867ms
Avec if: 2817ms
Avec if: 2817ms

Le résultat avec une Uno et l'opérateur ternaire:

Le croquis utilise 2444 octets (7%) de l'espace de stockage de programmes. Le maximum est de 32256 octets.
Les variables globales utilisent 216 octets (10%) de mémoire dynamique, ce qui laisse 1832 octets pour les variables locales. Le maximum est de 2048 octets.
Avec ?:; : 2665ms
Avec ?:; : 2666ms
Avec ?:; : 2817ms
Avec ?:; : 2868ms
Avec ?:; : 2867ms
Avec ?:; : 2867ms
Avec ?:; : 2817ms
Avec ?:; : 2816ms

Personnellement j'en pense que le if et l'opérateur ternaire sont la même chose et sont compilés de la même façon. L'avantage de l'opérateur ternaire est que mon compilateur ne râle pas si on ne rajoute pas des {}. Dans mon code je mets {{ quand je n'en mettrais pas mais le compilateur n'est pas content. Mais je ne suis pas sûr que l'opérateur ternaire est plus lisible. Je l'utilise plutôt quand j'ai une affectation conditionnelle.

Le switch semble utiliser moins de code, et semble prendre plus de temps.

1 Like

Bonjour @vileroi

Merci pour ton retour
Ici l’utilisation de l’opérateur ternaire est originale (pour moi) :

Bonne journée à toi

Après relecture, il ne sert à rien ?

Idem :

et enfin :

// N = 6 ou 7
(N==6)? resultat += 1: resultat += 8;

Sinon belle démonstration

Oui, c'est un random(8) qu'il faut mettre pour avoir un nombre de 0 à 7.... Mea culpa. Mais cela ne change rien aux mesures.

Je n'ai pas testé le if avec une structure genre

if (N==0) resultat += 4;
else if (N==1)  resultat += 6;
else if (N==2)  resultat += 9;
else ...

La structure que j'utilise exécute systématiquement 3 tests. En faisant comme au dessus, on en utiliserait entre 1 et 7, ce qui ferait une moyenne de 4, donc prendrait en moyenne plus de temps. Par contre pour le code cela ne doit pas le rallonger car il y a an tout 6 comparaisons au lieu de 7.

volatile uint8_t N, resultat; // Pour pouvoir faire les tests, volatile pour éviter les optimisations
volatile long temps, // Mesure du temps avec millis() 
              boucle; // Nombre de boucles pour avoir une certaine précision

void setup()
{
  Serial.begin(115200); // Initialisation de la console
}

void loop()
{
  Serial.print("Avec if: ");
  N = random(8);
  temps = millis(); // Début de la mesure
  for (boucle = 0; boucle < 800000L; boucle++) {
    if (N < 4) {
      if (N < 2) {
        if (N == 0)
          resultat += 4;
        else
          resultat += 6;
      } else {
        if (N == 2)
          resultat += 9;
        else
          resultat += 8;
      }
    } else {
      if (N < 6) {
        if (N == 4)
          resultat += 2;
        else
          resultat += 7;
      } else {
        if (N == 6)
          resultat += 1;
        else
          resultat += 8;  
      }
    }

  }
   // Calcul avec "resultat" (qui vaut forcément 0) pour éviter un code inutile qui peut être supprimé sans avertissement 
  Serial.print(millis() - temps + resultat); // Fin de la mesure
  Serial.println("ms");
}

Ici si je ne me suis pas planté :

Avec if: 2917ms
Avec if: 2817ms
Avec if: 2817ms
Avec if: 2817ms
Avec if: 2816ms
Avec if: 2665ms
Avec if: 2667ms
Avec if: 2867ms
Avec if: 2866ms
Avec if: 2868ms
Avec if: 2666ms
Avec if: 2867ms
Avec if: 2817ms

et :

Les variables globales utilisent 214 octets (10%) de mémoire dynamique, ce qui laisse 1834 octets pour les variables locales. Le maximum est de 2048 octets.

Mais c'est la même chose que toi sauf erreurs...
Et au demeurant ce code est le même en un peu plus ressemblant au tien :

volatile uint8_t N, resultat; // Pour pouvoir faire les tests, volatile pour éviter les optimisations
volatile long temps, // Mesure du temps avec millis() 
              boucle; // Nombre de boucles pour avoir une certaine précision

void setup()
{
  Serial.begin(115200); // Initialisation de la console
}

void loop()
{
  Serial.print("Avec if: ");
  N = random(8);
  temps = millis(); // Début de la mesure
  for (boucle = 0; boucle < 800000L; boucle++) if (N < 4)
     {
      if (N < 2) {
        if (N == 0)
          resultat += 4;
        else
          resultat += 6;
      } else {
        if (N == 2)
          resultat += 9;
        else
          resultat += 8;
      }
    } else {
      if (N < 6) {
        if (N == 4)
          resultat += 2;
        else
          resultat += 7;
      } else {
        if (N == 6)
          resultat += 1;
        else
          resultat += 8;   // jamais atteint
      }
    }


   // Calcul avec "resultat" (qui vaut forcément 0) pour éviter un code inutile qui peut être supprimé sans avertissement 
  Serial.print(millis() - temps + resultat); // Fin de la mesure
  Serial.println("ms");
}

Enfin ce que tu demandes :

#include <Arduino.h>

volatile uint8_t N, resultat;   
volatile long     temps, boucle;

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

void loop() {
  Serial.print("Avec if : ");

  N     = random(8);       
  temps = millis();         

  for (boucle = 0; boucle < 800000L; boucle++)
{
    if      (N == 0) resultat += 4;
    else if (N == 1) resultat += 6;
    else if (N == 2) resultat += 9;
    else if (N == 3) resultat += 8;
    else if (N == 4) resultat += 2;
    else if (N == 5) resultat += 7;
    else if (N == 6) resultat += 1;
    else              resultat += 8;     
}
  
  Serial.print(millis() - temps + resultat);
  Serial.println(" ms");
}

ce qui revient au même que :

#include <Arduino.h>

volatile uint8_t N, resultat;   
volatile long     temps, boucle;

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

void loop() {
  Serial.print("Avec if : ");

  N     = random(8);       
  temps = millis();         

  for (boucle = 0; boucle < 800000L; boucle++)
    if      (N == 0) resultat += 4;
    else if (N == 1) resultat += 6;
    else if (N == 2) resultat += 9;
    else if (N == 3) resultat += 8;
    else if (N == 4) resultat += 2;
    else if (N == 5) resultat += 7;
    else if (N == 6) resultat += 1;
    else              resultat += 8;     
  
  Serial.print(millis() - temps + resultat);
  Serial.println(" ms");
}

Avec switch: 3168ms
Avec switch: 3169ms
Avec switch: 3169ms
Avec switch: 3169ms
Avec switch: 3170ms
Avec switch: 3068ms
Avec switch: 3170ms
Avec switch: 3169ms
Avec switch: 3169ms
Avec switch: 3170ms
Avec switch: 3069ms
Avec switch: 3169ms
Avec switch: 3169ms

Le croquis utilise 2398 octets (7%) de l'espace de stockage de programmes. Le maximum est de 32256 octets.
Les variables globales utilisent 218 octets (10%) de mémoire dynamique, ce qui laisse 1830 octets pour les variables locales. Le maximum est de 2048 octets.

Bon après je ne suis pas aux millisecondes près. J'aime bien ta démo, ça m'a permis entre autre d'aller encore plus loin avec l'utilisation des opérateurs ternaires en cascade :wink:
Merci à toi
Bonne journée.

Bonjour,

Je préfère de loin cette implémentation à temps constant et à priori plus rapide:

...
  uint8_t offset[8] = { 4, 6, 9, 8, 2, 7, 1, 8 };
...
  for (boucle = 0; boucle < 800000L; boucle++) {
      resultat += ((N >= (sizeof(offset) / sizeof(offset[0])))
                ? offset[(sizeof(offset) / sizeof(offset[0])) - 1]
                : offset[N]);
  }
...

NB: Je privilégie dès que possible des exécutions à temps constant

A suivre...

Bonjour @claudius01
Pour le coup, j'ai posé la question à ChatGPT et voici la réponse et le code :
accès direct dans un tableau

— compacte, très rapide, zéro branchement à l’intérieur de la boucle —

#include <Arduino.h>

volatile uint8_t N, resultat;                 // variables protégées de l’optimiseur
volatile long     temps, boucle;              // chrono et compteur

/* Table des incréments                0   1   2   3   4   5   6   7  */
const uint8_t offset[8] =             { 4,  6,  9,  8,  2,  7,  1,  8 };

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

void loop() {
  Serial.print("Avec table : ");

  N     = random(8);                          // 0-7 inclus
  temps = millis();                           // départ chrono

  for (boucle = 0; boucle < 800000L; ++boucle) {

      /*  ── Sélection de l’incrément ────────────────
          – si N ∈ 0-7  → offset[N]
          – sinon       → offset[7]  (sécurité)      */
      resultat += (N < (sizeof offset))       /* sizeof offset == 8 */
                  ? offset[N]
                  : offset[7];
  }

  /* ‘resultat’ retombe à 0 modulo 256, mais empêche la boucle d’être supprimée */
  Serial.print(millis() - temps + resultat);
  Serial.println(" ms");
}
Point Explication
Tableau offset index 0 → +4, index 1 → +6, …, index 7 → +8 — correspond exactement aux anciennes branches.
Ternaire N < 8 ? offset[N] : offset[7] : on protège contre une valeur hors plage sans ralentir le cas normal (sur Uno le compilateur supprime la comparaison si N est réputé valoir 0-7).
Performances Dans la boucle : une seule lecture mémoire + une addition 8-bit. Plus rapide qu’un switch ou que des if, et plus court en flash qu’une table de sauts.
Résultat modulo 256 Comme auparavant, tout déborde à 0 (chaque incrément × 800 000 est multiple de 256).
Sécurité optionnelle Si vous êtes certain que N reste toujours 0-7, vous pouvez simplifier :
resultat += offset[N];

-->

#include <Arduino.h>

/* Table des incréments
   index : 0  1  2  3  4  5  6  7     */
const uint8_t offset[8] = { 4, 6, 9, 8, 2, 7, 1, 8 };

volatile uint8_t N, resultat;      // ‘volatile’ → pas d’optimisation
volatile long     temps, boucle;

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

void loop() {
  Serial.print("Avec table directe : ");

  N     = random(8);               // N ∈ 0…7
  temps = millis();                // chrono départ

  for (boucle = 0; boucle < 800000L; ++boucle)
    resultat += offset[N];         // accès direct, aucune branche

  /* ‘resultat’ ⇢ 0 modulo 256 mais oblige le compilateur
     à conserver la boucle entière                                   */
  Serial.print(millis() - temps + resultat);
  Serial.println(" ms");
}

  • Aucune condition dans la boucle : juste une lecture de tableau et une addition → c’est la version la plus rapide et la plus compacte en flash.
  • Assure-toi simplement que N reste bien dans 0-7 (ici c’est garanti par random(8)).

Test du dernier programme :

Le croquis utilise 2370 octets (7%) de l'espace de stockage de programmes. Le maximum est de 32256 octets.
Les variables globales utilisent 236 octets (11%) de mémoire dynamique, ce qui laisse 1812 octets pour les variables locales. Le maximum est de 2048 octets.

Avec table directe : 2364 ms
Avec table directe : 2365 ms
Avec table directe : 2364 ms
Avec table directe : 2364 ms
Avec table directe : 2364 ms
Avec table directe : 2365 ms
Avec table directe : 2364 ms
Avec table directe : 2364 ms
Avec table directe : 2365 ms
Avec table directe : 2364 ms
Avec table directe : 2364 ms
Avec table directe : 2364 ms
Avec table directe : 2365 ms
Avec table directe : 2364 ms
Avec table directe : 2364 ms

Donc plus rapide mais plus gourmand en mémoire. A voir celui qui privilégie la vitesse ou la mémoire ...

Bonne journée.

PS :
Après il précise que si vous êtes certain que N reste toujours 0-7, vous pouvez simplifier :

Comment ne peut-on pas en être certain :joy:

Oui et non, un IF ternaire renvois une valeur, donc c'est un peu plus qu'un IF, mais c'est quand même très léger.

C'est surtout un gout personnel, il faut donc que cela te parles avant tout ou que tu es besoin de coder en groupe et donc d'avoir des règles communes, facilitant la lecture de tous.
Personnellement:

  • si j'ai des tests sur la même variable, je trouves le switch plus lisible.
  • si j'ai une affectation d'une variable en fonction d'une condition, je trouve le IF unaire est plus lisible.

Je n'utilise jamais un if unaire, si j'utilise pas le retour.
par exemple

(a>b) ? Serial.println("a supérieur à b"): Serial.println("b supérieur ou égale à a");