La question du comportement indéfini (Undefined Behavior)

Un cas bien connu de comportement indéfini résulte de débordements lors d'opérations arithmétiques sur des entiers : ../Sujets/Maths/Nombres-entiers.html#debordement

Plusieurs langages n'ont pas le concept de comportement indéfini, ou Undefined Behavior, mais ce concept est important dans un langage axé sur la vitesse d'exécution à tout prix (ou presque) comme C ou C++. Dans ces langages, en effet, la plupart des expressions et des énoncés admissibles mènent à un comportement défini par le standard du langage, mais certaines opérations ont un résultat non-spécifié ou indéfini, donc pour lesquels les choix peuvent varier d'un compilateur à l'autre.

La définition technique du comportement indéfini en C++ est : http://eel.is/c++draft/defns.undefined

Le présent document propose quelques lectures pour mieux comprendre ce que signifie le terme Undefined Behavior, qui contribue à l'atteinte (ou non) d'une forme de Type-Safety dans un langage.

« Undefined behavior isn't evil. Its chaotic neutral » – Bryce Lelbach (Source)

Quelques exemples de comportement indéfini et de ses impacts, empruntés pour la plupart à Ville Voutilainen.

Dans cet exemple, le code généré n'appellera jamais h() car *p a été utilisé au prélable sans valider que p soit non-nul (voir https://godbolt.org/z/uDUwV5 pour une démonstration). Le if() qui semble redondant sur cette base sera éliminé. Aux yeux du compilateur, sur la base du code source, il est clair que p est non-nul

C'est un cas où le passé influence l'avenir

void g(int);
void h();
void f(int* p) {
   g(*p);
   if (!p)
      h();
}

Dans cet exemple (la différence avec le précédent n'est que d'un seul caractère!), l'appel à h() sera toujours fait et le test de p sera élidé (voir https://godbolt.org/z/e6rNPq pour une démonstration). Le raisonnement derrière cette optimisation sera essentiellement le même que pour l'exemple précédent

C'est un autre cas où le passé influence l'avenir

void g(int);
void h();
void f(int* p) {
   g(*p);
   if (p)
      h();
}

Dans cet exemple, l'appel à h() ne sera jamais fait et le test de p sera élidé, car plus tard dans la fonction, on utilise *p sans avoir validé p (voir https://godbolt.org/z/OXHyd- pour une démonstration). Aux yeux du compilateur, sur la base du code source, il est clair que p est non-nul

C'est un cas où le futur influence le passé

void g(int);
void h() {}
void f(int* p) {
   if (!p)
      h();
   g(*p);
}

Dans cet exemple, l'appel à h() sera toujours fait et le test de p sera élidé, car plus tard dans la fonction, on utilise *p sans avoir validé p (voir https://godbolt.org/z/AqebPk pour une démonstration). Aux yeux du compilateur, sur la base du code source, il est clair que p est non-nul

C'est un autre cas où le futur influence le passé

void g(int);
void h() {}
void f(int* p) {
   if (p)
      h();
   g(*p);
}

Exemple plus complexe (voir https://godbolt.org/z/Nm-GOj pour une démonstration) :

  • L'appel à h(x) n'aura jamais lieu car le test if(!p) sera considéré inutile (toujours faux) du fait que g(*p) est fait sans validation de p
  • L'appel à g(x) n'aura jamais lieu car if(x==5) sera toujours faux, du fait que l'appel à h(x) n'aura jamais lieu...
  • Si le compilateur n'a pas plus de contexte, la fonction g() générée sera... vide, faute d'avoir des conséquences sur le code qui sera exécuté

Les compilateurs sont devenus très, très efficaces...

static int y = 42;
void g(int xx) { y = xx; }
void h(int& x) { x = 5; }
void yet_another_stop_evading_me_compiler(int);
void f(int* p) {
    int x = 42;
    if (!p) {
        h(x);
    }
    if (x == 5)
        g(x);
    g(*p);
    yet_another_stop_evading_me_compiler(y);
}

Exemple intéressant proposé par Andrzej Krzemienski (voir https://godbolt.org/z/62VleR pour une démonstration). Le programme souhaite comptabiliser les appels à f(p) p est nul, or f(p) appelle impl(p) qui, pour sa part, utilise *p sans valider que p soit non-nul. Le futur influence le passé, et le compilateur supprimera le test sur p fait par f() du fait qu'il semble manifestement inutile

int error_count = 0;
namespace {
   void log_error() { 
      ++error_count; // this has side effect
   }                 // but is guaranteed to return normally
   int impl(int* p) {
      // implicit assumption: p != nullptr
      return *p;
   }
}
int f(int* p) {
   if (p == nullptr) // this will get ellided
      log_error();   //   due to the implicit assumption
   return impl(p);
}

Valid XHTML 1.0 Transitional

CSS Valide !