Namespaces
Variants

Undefined behavior

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

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.
  • erroneous behavior - Le comportement (incorrect) qu'il est recommandé à l'implémentation de diagnostiquer.
  • Le comportement erroné est toujours la conséquence d'un code de programme incorrect.
  • L'évaluation d'une expression constante ne résulte jamais en un comportement erroné.
  • Si l'exécution contient une opération spécifiée comme ayant un comportement erroné, l'implémentation est autorisée et recommandée d'émettre un diagnostic, et est autorisée à terminer l'exécution à un moment non spécifié après cette opération.
  • Une implémentation peut émettre un diagnostic si elle peut déterminer qu'un comportement erroné est atteignable sous un ensemble d'hypothèses spécifique à l'implémentation concernant le comportement du programme, ce qui peut entraîner des faux positifs.
Exemples de comportement erroné
#include <cassert>
#include <cstring>
void f()
{   
    int d1, d2;       // d1, d2 ont des valeurs erronées
    int e1 = d1;      // comportement erroné
    int e2 = d1;      // comportement erroné
    assert(e1 == e2); // tient
    assert(e1 == d1); // tient, comportement erroné
    assert(e2 == d1); // tient, comportement erroné
    std::memcpy(&d2, &d1, sizeof(int)); // pas de comportement erroné, mais
                                        // d2 a une valeur erronée
    assert(e1 == d2); // tient, comportement erroné
    assert(e2 == d2); // tient, comportement erroné
}
unsigned char g(bool b)
{
    unsigned char c;     // c a une valeur erronée
    unsigned char d = c; // pas de comportement erroné, mais d a une valeur erronée
    assert(c == d);      // tient, les deux promotions entières ont un comportement erroné
    int e = d;           // comportement erroné
    return b ? d : 0;    // comportement erroné si b est vrai
}
(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.
  • comportement indéfini à l'exécution - Le comportement qui n'est pas défini sauf lorsqu'il se produit lors de l'évaluation d'une expression en tant que expression constante de base .
(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

#include <cstdio>
int main()
{
    bool p; // uninitialized local variable
    if (p)  // UB access to uninitialized scalar
        std::puts("p is true");
    if (!p) // UB access to uninitialized scalar
        std::puts("p is false");
}

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
  • Norme C++23 (ISO/IEC 14882:2024) :
  • 3.25 programme mal formé [defns.ill.formed]
  • 3.26 comportement défini par l'implémentation [defns.impl.defined]
  • 3.66 comportement non spécifié [defns.unspecified]
  • 3.68 programme bien formé [defns.well.formed]
  • Norme C++20 (ISO/IEC 14882:2020) :
  • TBD programme mal formé [defns.ill.formed]
  • TBD comportement défini par l'implémentation [defns.impl.defined]
  • TBD comportement non spécifié [defns.unspecified]
  • TBD programme bien formé [defns.well.formed]
  • Norme C++17 (ISO/IEC 14882:2017) :
  • TBD programme mal formé [defns.ill.formed]
  • TBD comportement défini par l'implémentation [defns.impl.defined]
  • TBD comportement non spécifié [defns.unspecified]
  • TBD programme bien formé [defns.well.formed]
  • Norme C++14 (ISO/IEC 14882:2014) :
  • TBD programme mal formé [defns.ill.formed]
  • TBD comportement défini par l'implémentation [defns.impl.defined]
  • TBD comportement non spécifié [defns.unspecified]
  • TBD programme bien formé [defns.well.formed]
  • Norme C++11 (ISO/IEC 14882:2011) :
  • TBD programme mal formé [defns.ill.formed]
  • TBD comportement défini par l'implémentation [defns.impl.defined]
  • TBD comportement non spécifié [defns.unspecified]
  • TBD programme bien formé [defns.well.formed]
  • Norme C++98 (ISO/IEC 14882:1998) :
  • TBD programme mal formé [defns.ill.formed]
  • TBD comportement défini par l'implémentation [defns.impl.defined]
  • TBD comportement non spécifié [defns.unspecified]
  • TBD programme bien formé [defns.well.formed]

Voir aussi

[[ assume ( expression )]]
(C++23)
spécifie que l' expression sera toujours évaluée à true à un point donné
(spécificateur d'attribut)
(C++26)
spécifie qu'un objet a une valeur indéterminée s'il n'est pas initialisé
(spécificateur d'attribut)
marque un point d'exécution inaccessible
(fonction)
Documentation C pour Comportement indéfini

Liens externes

1. Blog du projet LLVM : Ce que tout programmeur C devrait savoir sur le comportement indéfini #1/3
2. Blog du projet LLVM : Ce que tout programmeur C devrait savoir sur le comportement indéfini #2/3
3. Blog du projet LLVM : Ce que tout programmeur C devrait savoir sur le comportement indéfini #3/3
4. Le comportement indéfini peut entraîner des voyages dans le temps (entre autres choses, mais le voyage dans le temps est le plus insolite)
5. Comprendre le dépassement d'entier en C/C++
6. S'amuser avec les pointeurs NULL, partie 1 (exploit local dans Linux 2.6.30 causé par un comportement indéfini dû à un déréférencement de pointeur nul)
7. Comportement indéfini et dernier théorème de Fermat
8. Guide du programmeur C++ sur le comportement indéfini