Undefined behavior
Rend l'ensemble du programme dénué de sens si certaines règles du langage sont violées.
Table des matières |
Explication
La norme C++ définit précisément le comportement observable de chaque programme C++ qui ne relève pas de l'une des catégories suivantes :
- ill-formed - Le programme contient des erreurs de syntaxe ou des erreurs sémantiques détectables.
-
- Un compilateur C++ conforme est tenu d'émettre un diagnostic, même s'il définit une extension de langage qui attribue un sens à un tel code (comme avec les tableaux de taille variable).
- Le texte de la norme utilise shall , shall not , et ill-formed pour indiquer ces exigences.
- mal formé, aucun diagnostic requis - Le programme contient des erreurs sémantiques qui peuvent ne pas être diagnostiquables dans le cas général (par exemple, des violations de la ODR ou d'autres erreurs uniquement détectables au moment de l'édition des liens).
-
- Le comportement est indéfini si un tel programme est exécuté.
- comportement défini par l'implémentation - Le comportement du programme varie entre les implémentations, et l'implémentation conforme doit documenter les effets de chaque comportement.
-
- Par exemple, le type de std::size_t ou le nombre de bits dans un octet, ou le texte de std::bad_alloc::what .
- Un sous-ensemble du comportement défini par l'implémentation est le comportement spécifique aux paramètres régionaux , qui dépend des paramètres régionaux fournis par l'implémentation.
- comportement non spécifié - Le comportement du programme varie entre les implémentations, et l'implémentation conforme n'est pas tenue de documenter les effets de chaque comportement.
-
- Par exemple, order of evaluation , si des string literals identiques sont distincts, la quantité de surcharge d'allocation de tableau, etc.
- Chaque comportement non spécifié produit l'un d'un ensemble de résultats valides.
|
(depuis C++26) |
- comportement indéfini - Il n'y a aucune restriction sur le comportement du programme.
-
- Quelques exemples de comportement indéfini sont les courses de données, les accès mémoire en dehors des limites des tableaux, le dépassement d'entier signé, le déréférencement de pointeur nul, plusieurs modifications du même scalaire dans une expression sans aucun point de séquence intermédiaire (jusqu'à C++11) non séquencées (depuis C++11) , l'accès à un objet via un pointeur d'un type différent , etc.
- Les implémentations ne sont pas tenues de diagnostiquer le comportement indéfini (bien que de nombreuses situations simples soient diagnostiquées), et le programme compilé n'est pas requis de faire quoi que ce soit de significatif.
|
(depuis C++11) |
UB et optimisation
Parce que les programmes C++ corrects sont exempts de comportement indéfini, les compilateurs peuvent produire des résultats inattendus lorsqu'un programme qui présente réellement un UB est compilé avec l'optimisation activée :
Par exemple,
Dépassement signé
int foo(int x) { return x + 1 > x; // soit vrai soit UB en raison d'un dépassement signé }
peut être compilé comme ( démo )
foo(int): mov eax, 1 ret
Accès hors limites
int table[4] = {}; bool exists_in_table(int v) { // retourne true dans l'une des 4 premières itérations ou UB en raison d'un accès hors limites for (int i = 0; i <= 4; i++) if (table[i] == v) return true; return false; }
Peut être compilé comme ( démo )
exists_in_table(int): mov eax, 1 ret
Scalaire non initialisé
std::size_t f(int x) { std::size_t a; if (x) // soit x non nul, soit comportement indéfini a = 42; return a; }
Peut être compilé comme ( démo )
f(int): mov eax, 42 ret
La sortie affichée a été observée sur une ancienne version de gcc
Sortie possible :
p is true p is false
Scalaire invalide
int f() { bool b = true; unsigned char* p = reinterpret_cast<unsigned char*>(&b); *p = 10; // la lecture de b est maintenant UB return b == 0; }
Peut être compilé comme ( demo )
f(): mov eax, 11 ret
Déréférencement de pointeur nul
Les exemples illustrent la lecture à partir du résultat de la déréférenciation d'un pointeur nul.
int foo(int* p) { int x = *p; if (!p) return x; // Soit un comportement indéfini ci-dessus, soit cette branche n'est jamais exécutée else return 0; } int bar() { int* p = nullptr; return *p; // Comportement indéfini inconditionnel }
peut être compilé comme ( démo )
foo(int*): xor eax, eax ret bar(): ret
Accès au pointeur passé à std::realloc
Choisissez clang pour observer le résultat affiché
#include <cstdlib> #include <iostream> int main() { int* p = (int*)std::malloc(sizeof(int)); int* q = (int*)std::realloc(p, sizeof(int)); *p = 1; // UB access to a pointer that was passed to realloc *q = 2; if (p == q) // UB access to a pointer that was passed to realloc std::cout << *p << *q << '\n'; }
Résultat possible :
12
Boucle infinie sans effets secondaires
Choisissez clang ou le dernier gcc pour observer le résultat affiché.
#include <iostream> bool fermat() { const int max_value = 1000; // Non-trivial infinite loop with no side effects is UB for (int a = 1, b = 1, c = 1; true; ) { if (((a * a * a) == ((b * b * b) + (c * c * c)))) return true; // disproved :() a++; if (a > max_value) { a = 1; b++; } if (b > max_value) { b = 1; c++; } if (c > max_value) c = 1; } return false; // not disproved } int main() { std::cout << "Fermat's Last Theorem "; fermat() ? std::cout << "has been disproved!\n" : std::cout << "has not been disproved.\n"; }
Résultat possible :
Fermat's Last Theorem has been disproved!
Mal formé avec message de diagnostic
Notez que les compilateurs sont autorisés à étendre le langage de manière à donner un sens aux programmes mal formés. La seule exigence du standard C++ dans de tels cas est un message de diagnostic (avertissement du compilateur), sauf si le programme était "mal formé sans diagnostic requis".
Par exemple, sauf si les extensions de langage sont désactivées via
--pedantic-errors
, GCC compilera l'exemple suivant
avec seulement un avertissement
même s'il
apparaît dans le standard C++
comme un exemple d'"erreur" (voir aussi
GCC Bugzilla #55783
)
#include <iostream> // Exemple de modification, ne pas utiliser de constante double a{1.0}; // Norme C++23, §9.4.5 Initialisation par liste [dcl.init.list], Exemple #6 : struct S { // pas de constructeurs à liste d'initialisation S(int, double, double); // #1 S(); // #2 // ... }; S s1 = {1, 2, 3.0}; // OK, invoque #1 S s2{a, 2, 3}; // erreur : rétrécissement S s3{}; // OK, invoque #2 // — fin de l'exemple] S::S(int, double, double) {} S::S() {} int main() { std::cout << "All checks have passed.\n"; }
Sortie possible :
main.cpp:17:6: error: type 'double' cannot be narrowed to 'int' in initializer ⮠
list [-Wc++11-narrowing]
S s2{a, 2, 3}; // error: narrowing
^
main.cpp:17:6: note: insert an explicit cast to silence this issue
S s2{a, 2, 3}; // error: narrowing
^
static_cast<int>( )
1 error generated.
Références
| Contenu étendu |
|---|
|
Voir aussi
[[
assume
(
expression
)]]
(C++23)
|
spécifie que l'
expression
sera toujours évaluée à
true
à un point donné
(spécificateur d'attribut) |
[[
indeterminate
]]
(C++26)
|
spécifie qu'un objet a une valeur indéterminée s'il n'est pas initialisé
(spécificateur d'attribut) |
|
(C++23)
|
marque un point d'exécution inaccessible
(fonction) |
|
Documentation C
pour
Comportement indéfini
|
|