C++
Comportement non défini
Recherche…
Introduction
Qu'est-ce qu'un comportement indéfini (UB)? Selon la norme ISO C ++ (§1.3.24, N4296), c'est "le comportement pour lequel la présente Norme internationale n'impose aucune exigence".
Cela signifie que lorsqu'un programme rencontre UB, il est autorisé à faire ce qu'il veut. Cela signifie souvent un accident, mais il peut simplement ne rien faire , faire en sorte que les démons ne vous touchent pas ou même sembler fonctionner correctement!
Inutile de dire que vous devriez éviter d'écrire du code qui appelle UB.
Remarques
Si un programme contient un comportement indéfini, le standard C ++ ne place aucune contrainte sur son comportement.
- Il peut sembler fonctionner comme prévu par le développeur, mais il peut également tomber en panne ou produire des résultats étranges.
- Le comportement peut varier entre les exécutions du même programme.
- Toute partie du programme peut ne pas fonctionner correctement, y compris les lignes précédant la ligne contenant un comportement indéfini.
- L'implémentation n'est pas requise pour documenter le résultat d'un comportement non défini.
Une implémentation peut documenter le résultat d'une opération qui produit un comportement indéfini conformément à la norme, mais un programme qui dépend de ce comportement documenté n'est pas portable.
Pourquoi un comportement indéfini existe
Intuitivement, le comportement non défini est considéré comme une mauvaise chose, car de telles erreurs ne peuvent être traitées gracieusement, par exemple, par des gestionnaires d'exceptions.
Mais laisser un comportement indéfini fait en réalité partie intégrante de la promesse de C ++: "vous ne payez pas pour ce que vous n'utilisez pas". Un comportement indéfini permet à un compilateur de supposer que le développeur sait ce qu'il fait et n'introduit pas de code pour vérifier les erreurs mises en évidence dans les exemples ci-dessus.
Trouver et éviter un comportement indéfini
Certains outils peuvent être utilisés pour découvrir un comportement indéfini pendant le développement:
- La plupart des compilateurs ont des drapeaux d'avertissement pour avertir de certains cas de comportement indéfini au moment de la compilation.
- Les versions plus récentes de gcc et de clang incluent un indicateur appelé "Désinfectant de comportement
-fsanitize=undefined
" (-fsanitize=undefined
) qui vérifie le comportement indéfini à l'exécution, à un coût de performance. -
lint
outils ressemblant à deslint
peuvent effectuer une analyse comportementale non définie plus approfondie.
Comportement non défini , non spécifié et défini par l'implémentation
De la norme C ++ 14 (ISO / IEC 14882: 2014), section 1.9 (Exécution du programme):
Les descriptions sémantiques de la présente Norme internationale définissent une machine abstraite non déterministe paramétrée. [COUPER]
Certains aspects et opérations de la machine abstraite sont décrits dans la présente Norme internationale comme définis par la mise en œuvre (par exemple,
sizeof(int)
). Celles-ci constituent les paramètres de la machine abstraite . Chaque mise en œuvre doit inclure une documentation décrivant ses caractéristiques et son comportement à ces égards. [COUPER]Certains autres aspects et opérations de la machine abstraite sont décrits dans la présente Norme internationale comme non spécifiés (par exemple, évaluation des expressions dans un nouvel initialiseur si la fonction d'allocation ne parvient pas à allouer de la mémoire). Dans la mesure du possible, la présente Norme internationale définit un ensemble de comportements admissibles. Celles-ci définissent les aspects non déterministes de la machine abstraite. Une instance de la machine abstraite peut donc avoir plus d'une exécution possible pour un programme donné et une entrée donnée.
Certaines autres opérations sont décrites dans la présente Norme internationale comme non définies (ou exemple, l'effet de la tentative de modification d'un objet
const
). [ Note : la présente Norme internationale n'impose aucune exigence concernant le comportement des programmes qui contiennent un comportement indéfini. - note finale ]
Lecture ou écriture à travers un pointeur nul
int *ptr = nullptr;
*ptr = 1; // Undefined behavior
Il s'agit d' un comportement indéfini , car un pointeur null ne pointe sur aucun objet valide, il n'y a donc aucun objet à écrire dans *ptr
.
Bien que cela provoque le plus souvent une erreur de segmentation, elle est indéfinie et tout peut arriver.
Aucune déclaration de retour pour une fonction avec un type de retour non vide
L'omission de l'instruction return
dans une fonction dont le type de retour est non void
est un comportement indéfini .
int function() {
// Missing return statement
}
int main() {
function(); //Undefined Behavior
}
La plupart des compilateurs modernes émettent un avertissement au moment de la compilation pour ce type de comportement indéfini.
Note: main
est la seule exception à la règle. Si main
n'a pas d'instruction de return
, le compilateur insère automatiquement return 0;
pour vous, il peut donc être laissé de côté.
Modifier un littéral de chaîne
char *str = "hello world";
str[0] = 'H';
"hello world"
est un littéral de chaîne, donc le modifier donne un comportement indéfini.
L'initialisation de str
dans l'exemple ci-dessus était officiellement obsolète (il est prévu de la supprimer d'une future version du standard) en C ++ 03. Un certain nombre de compilateurs avant 2003 pourraient émettre un avertissement à ce sujet (par exemple, une conversion suspecte). Après 2003, les compilateurs avertissent généralement d'une conversion obsolète.
L'exemple ci-dessus est illégal et entraîne un diagnostic du compilateur, en C ++ 11 et versions ultérieures. Un exemple similaire peut être construit pour présenter un comportement indéfini en permettant explicitement la conversion de type, telle que:
char *str = const_cast<char *>("hello world");
str[0] = 'H';
Accéder à un index hors limites
C'est un comportement indéfini pour accéder à un index qui est hors limites pour un tableau (ou un conteneur de bibliothèque standard, car ils sont tous implémentés en utilisant un tableau brut ):
int array[] = {1, 2, 3, 4, 5};
array[5] = 0; // Undefined behavior
Il est permis d'avoir un pointeur pointant vers la fin du tableau (dans ce cas, array + 5
), vous ne pouvez pas le déréférencer, car ce n'est pas un élément valide.
const int *end = array + 5; // Pointer to one past the last index
for (int *p = array; p != end; ++p)
// Do something with `p`
En général, vous n'êtes pas autorisé à créer un pointeur hors limites. Un pointeur doit pointer sur un élément du tableau ou sur un élément passé.
Division entière par zéro
int x = 5 / 0; // Undefined behavior
La division par 0
est mathématiquement indéfinie et, en tant que telle, il est logique qu'il s'agisse d'un comportement indéfini.
Toutefois:
float x = 5.0f / 0.0f; // x is +infinity
La plupart des implémentations implémentent IEEE-754, qui définit la division en virgule flottante par zéro pour renvoyer NaN
(si le numérateur est 0.0f
), l' infinity
(si le numérateur est positif) ou -infinity
(si le numérateur est négatif).
Dépassement d'entier signé
int x = INT_MAX + 1;
// x can be anything -> Undefined behavior
Si, lors de l'évaluation d'une expression, le résultat n'est pas défini mathématiquement ou ne figure pas dans la plage des valeurs représentables pour son type, le comportement n'est pas défini.
(Norme C ++ 11, paragraphe 5/4)
C’est l’un des plus désagréables, car il produit généralement un comportement reproductible et non écrasant, de sorte que les développeurs peuvent être tentés de s’appuyer fortement sur le comportement observé.
D'autre part:
unsigned int x = UINT_MAX + 1;
// x is 0
est bien défini depuis:
Les entiers non signés, déclarés non signés, doivent obéir aux lois de l'arithmétique modulo
2^n
oùn
est le nombre de bits dans la représentation des valeurs de la taille entière entière.
(Norme C ++ 11, paragraphe 3.9.1 / 4)
Parfois, les compilateurs peuvent exploiter un comportement indéfini et optimiser
signed int x ;
if(x > x + 1)
{
//do something
}
Dans la mesure où un dépassement d'entier signé n'est pas défini, le compilateur est libre de supposer qu'il peut ne jamais se produire et peut donc optimiser le bloc "if"
Utilisation d'une variable locale non initialisée
int a;
std::cout << a; // Undefined behavior!
Cela se traduit par un comportement indéfini , car a
est non initialisé.
Il est souvent, à tort, prétendu que c'est parce que la valeur est "indéterminée", ou "quelle que soit la valeur présente dans cet emplacement de mémoire avant". Cependant, c'est l'acte d'accéder à la valeur d' a
dans l'exemple ci-dessus qui donne un comportement indéfini. En pratique, imprimer un "garbage value" est un symptôme courant dans ce cas, mais ce n’est qu’une forme possible de comportement non défini.
Bien que très improbable dans la pratique (puisqu'il dépend d'un support matériel spécifique), le compilateur pourrait également électrocuter le programmeur lors de la compilation de l'exemple de code ci-dessus. Avec un tel support de compilateur et de matériel, une telle réponse à un comportement indéfini augmenterait nettement la compréhension moyenne (vivante) du programmeur de la véritable signification du comportement indéfini - à savoir que la norme n'impose aucune contrainte au comportement résultant.
L'utilisation d'une valeur indéterminée de type unsigned char
ne produit pas un comportement indéfini si la valeur est utilisée comme:
- le deuxième ou le troisième opérande de l'opérateur conditionnel ternaire;
- le bon opérande de l'opérateur de virgule intégré;
- l'opérande d'une conversion en caractère
unsigned char
; - l'opérande droit de l'opérateur d'affectation, si l'opérande gauche est également du type
unsigned char
; - l'initialiseur d'un objet
unsigned char
;
ou si la valeur est ignorée. Dans de tels cas, la valeur indéterminée se propage simplement au résultat de l'expression, le cas échéant.
Notez qu'une variable static
est toujours initialisée à zéro (si possible):
static int a;
std::cout << a; // Defined behavior, 'a' is 0
Plusieurs définitions non identiques (la règle de définition unique)
Si une classe, une énumération, une fonction en ligne, un modèle ou un membre d'un modèle possède un lien externe et est défini dans plusieurs unités de traduction, toutes les définitions doivent être identiques ou non définies conformément à la règle ODR .
foo.h
:
class Foo {
public:
double x;
private:
int y;
};
Foo get_foo();
foo.cpp
:
#include "foo.h"
Foo get_foo() { /* implementation */ }
main.cpp
:
// I want access to the private member, so I am going to replace Foo with my own type
class Foo {
public:
double x;
int y;
};
Foo get_foo(); // declare this function ourselves since we aren't including foo.h
int main() {
Foo foo = get_foo();
// do something with foo.y
}
Le programme ci-dessus présente un comportement indéfini car il contient deux définitions de la classe ::Foo
, qui a un lien externe, dans différentes unités de traduction, mais les deux définitions ne sont pas identiques. Contrairement à la redéfinition d'une classe dans la même unité de traduction, le compilateur n'a pas besoin de diagnostiquer ce problème.
Appariement incorrect de l'allocation de mémoire et de la désallocation
Un objet ne peut être désalloué que par delete
s'il a été alloué par new
et n'est pas un tableau. Si l'argument à delete
n'a pas été renvoyé par new
ou s'il s'agit d'un tableau, le comportement n'est pas défini.
Un objet ne peut être désalloué que par delete[]
s'il a été alloué par new
et est un tableau. Si l'argument de delete[]
n'a pas été renvoyé par new
ou n'est pas un tableau, le comportement est indéfini.
Si l'argument à free
n'a pas été renvoyé par malloc
, le comportement est indéfini.
int* p1 = new int;
delete p1; // correct
// delete[] p1; // undefined
// free(p1); // undefined
int* p2 = new int[10];
delete[] p2; // correct
// delete p2; // undefined
// free(p2); // undefined
int* p3 = static_cast<int*>(malloc(sizeof(int)));
free(p3); // correct
// delete p3; // undefined
// delete[] p3; // undefined
Ces problèmes peuvent être évités en évitant complètement les programmes malloc
et free
dans C ++, préférant les pointeurs intelligents de la bibliothèque standard sur raw new
et delete
, et préférant std::vector
et std::string
sur raw new
et delete[]
.
Accéder à un objet avec le mauvais type
Dans la plupart des cas, il est illégal d'accéder à un objet d'un type comme s'il s'agissait d'un type différent (sans tenir compte des qualificatifs cv). Exemple:
float x = 42;
int y = reinterpret_cast<int&>(x);
Le résultat est un comportement indéfini.
Il existe quelques exceptions à cette règle d' aliasing stricte :
- Un objet de type classe est accessible comme s'il s'agissait d'un type qui est une classe de base du type de classe réel.
- Vous pouvez accéder à n'importe quel type en tant que caractère
char
ouunsigned char
, mais l'inverse n'est pas vrai: il est impossible d'accéder à un tableau de caractères comme s'il s'agissait d'un type arbitraire. - Un type entier signé peut être accédé en tant que type non signé correspondant et vice versa .
Une règle connexe est que si une fonction membre non statique est appelée sur un objet qui n'a pas réellement le même type que la classe de définition de la fonction ou une classe dérivée, le comportement non défini se produit. Cela est vrai même si la fonction n'accède pas à l'objet.
struct Base {
};
struct Derived : Base {
void f() {}
};
struct Unrelated {};
Unrelated u;
Derived& r1 = reinterpret_cast<Derived&>(u); // ok
r1.f(); // UB
Base b;
Derived& r2 = reinterpret_cast<Derived&>(b); // ok
r2.f(); // UB
Débordement en virgule flottante
Si une opération arithmétique qui produit un type à virgule flottante produit une valeur qui ne se situe pas dans la plage des valeurs représentables du type de résultat, le comportement n'est pas défini conformément à la norme C ++, mais peut être défini par d'autres normes comme IEEE 754.
float x = 1.0;
for (int i = 0; i < 10000; i++) {
x *= 10.0; // will probably overflow eventually; undefined behavior
}
Appel de membres virtuels (purs) à partir d'un constructeur ou d'un destructeur
La norme (10.4) stipule:
Les fonctions membres peuvent être appelées à partir d'un constructeur (ou d'un destructeur) d'une classe abstraite; L'effet de faire un appel virtuel (10.3) à une fonction virtuelle pure directement ou indirectement pour l'objet créé (ou détruit) à partir d'un tel constructeur (ou destructeur) n'est pas défini.
Plus généralement, certaines autorités C ++, par exemple Scott Meyers, suggèrent de ne jamais appeler de fonctions virtuelles (même non pures) à partir de constructeurs et de constructeurs.
Prenons l'exemple suivant, modifié à partir du lien ci-dessus:
class transaction
{
public:
transaction(){ log_it(); }
virtual void log_it() const = 0;
};
class sell_transaction : public transaction
{
public:
virtual void log_it() const { /* Do something */ }
};
Supposons que nous créons un objet sell_transaction
:
sell_transaction s;
Cela appelle implicitement le constructeur de sell_transaction
, qui appelle d'abord le constructeur de transaction
. Cependant, lorsque le constructeur de transaction
est appelé, l'objet n'est pas encore du type sell_transaction
, mais uniquement de la transaction
type.
Par conséquent, l'appel de transaction::transaction()
à log_it
ne fera pas ce qui peut sembler être intuitif, à savoir appeler sell_transaction::log_it
.
Si
log_it
est purement virtuel, comme dans cet exemple, le comportement est indéfini.Si
log_it
est virtuel non pur,transaction::log_it
sera appelée.
Suppression d'un objet dérivé via un pointeur sur une classe de base sans destructeur virtuel.
class base { };
class derived: public base { };
int main() {
base* p = new derived();
delete p; // The is undefined behavior!
}
Dans la section [expr.delete] §5.3.5 / 3, la norme indique que si la delete
est appelée sur un objet dont le type statique ne comporte pas virtual
destructeur virtual
:
si le type statique de l'objet à supprimer est différent de son type dynamique, le type statique doit être une classe de base du type dynamique de l'objet à supprimer et le type statique doit avoir un destructeur virtuel ou le comportement n'est pas défini.
C'est le cas quelle que soit la question de savoir si la classe dérivée a ajouté des membres de données à la classe de base.
Accéder à une référence en suspens
Il est illégal d’accéder à une référence à un objet hors de portée ou détruit d’une autre manière. Une telle référence est dite pendante car elle ne fait plus référence à un objet valide.
#include <iostream>
int& getX() {
int x = 42;
return x;
}
int main() {
int& r = getX();
std::cout << r << "\n";
}
Dans cet exemple, la variable locale x
sort de la portée lorsque getX
retourne. (Notez que l' extension de durée de vie ne peut pas prolonger la durée de vie d'une variable locale au-delà de la portée du bloc dans lequel elle est définie). Par conséquent, r
est une référence en suspens. Ce programme a un comportement indéfini, bien qu'il puisse sembler fonctionner et imprimer 42
dans certains cas.
Extension de l'espace de noms `std` ou` posix`
La norme (17.6.4.2.1 / 1) interdit généralement l'extension de l'espace de noms std
:
Le comportement d'un programme C ++ n'est pas défini s'il ajoute des déclarations ou des définitions à l'espace de noms std ou à un espace de noms dans un espace de noms std, sauf indication contraire.
Il en va de même pour posix
(17.6.4.2.2 / 1):
Le comportement d'un programme C ++ n'est pas défini s'il ajoute des déclarations ou des définitions à posix d'espace de noms ou à un espace de noms dans posix d'espace de noms, sauf indication contraire.
Considérer ce qui suit:
#include <algorithm>
namespace std
{
int foo(){}
}
Rien dans la norme n'interdit à l' algorithm
(ou à l'un des en-têtes qu'il inclut) de définir la même définition, et ce code violerait donc la règle de définition unique .
Donc, en général, ceci est interdit. Il existe toutefois des exceptions spécifiques . Peut-être plus utile, il est permis d'ajouter des spécialisations pour les types définis par l'utilisateur. Par exemple, supposons que votre code a
class foo
{
// Stuff
};
Alors ce qui suit va bien
namespace std
{
template<>
struct hash<foo>
{
public:
size_t operator()(const foo &f) const;
};
}
Débordement lors de la conversion vers ou à partir du type à virgule flottante
Si, lors de la conversion de:
- un type entier à un type à virgule flottante,
- un type à virgule flottante pour un type entier, ou
- un type à virgule flottante pour un type à virgule flottante plus court,
la valeur source est en dehors de la plage de valeurs pouvant être représentée dans le type de destination, le résultat est un comportement non défini. Exemple:
double x = 1e100;
int y = x; // int probably cannot hold numbers that large, so this is UB
Jet statique de base à dérivé invalide
Si static_cast
est utilisé pour convertir un pointeur (resp. Référence) en une classe de base en un pointeur (resp. Référence) en une classe dérivée, mais l’opérande ne pointe pas (resp. Se référer) à un objet du type de classe dérivé, le comportement est indéfini Voir Base à la conversion dérivée .
Appel de fonction via le type de pointeur de fonction incompatible
Pour appeler une fonction via un pointeur de fonction, le type du pointeur de fonction doit correspondre exactement au type de la fonction. Sinon, le comportement est indéfini. Exemple:
int f();
void (*p)() = reinterpret_cast<void(*)()>(f);
p(); // undefined
Modifier un objet const
Toute tentative de modification d'un objet const
entraîne un comportement indéfini. Cela s'applique aux variables const
, aux membres des objets const
et aux membres de classe déclarés const
. (Cependant, un membre mutable
d'un objet const
n'est pas const
.)
Une telle tentative peut être faite via const_cast
:
const int x = 123;
const_cast<int&>(x) = 456;
std::cout << x << '\n';
Un compilateur inclura généralement la valeur d'un objet const int
, il est donc possible que ce code compile et imprime 123
. Les compilateurs peuvent également placer les valeurs des objets const
dans une mémoire en lecture seule, ce qui peut entraîner une erreur de segmentation. Dans tous les cas, le comportement n'est pas défini et le programme peut faire n'importe quoi.
Le programme suivant cache une erreur beaucoup plus subtile:
#include <iostream>
class Foo* instance;
class Foo {
public:
int get_x() const { return m_x; }
void set_x(int x) { m_x = x; }
private:
Foo(int x, Foo*& this_ref): m_x(x) {
this_ref = this;
}
int m_x;
friend const Foo& getFoo();
};
const Foo& getFoo() {
static const Foo foo(123, instance);
return foo;
}
void do_evil(int x) {
instance->set_x(x);
}
int main() {
const Foo& foo = getFoo();
do_evil(456);
std::cout << foo.get_x() << '\n';
}
Dans ce code, getFoo
crée un singleton de type const Foo
et son membre m_x
est initialisé à 123
. Ensuite, do_evil
est appelé et la valeur de foo.m_x
est apparemment passée à 456. Qu'est-ce qui n'a pas fonctionné?
En dépit de son nom, do_evil
ne fait rien de particulièrement mal; tout ce qu'il fait est d'appeler un passeur à travers un Foo*
. Mais ce pointeur pointe vers un objet const Foo
même si const_cast
n'a pas été utilisé. Ce pointeur a été obtenu via le constructeur de Foo
. Un const
objet ne devient pas const
jusqu'à ce que son initialisation est terminée, donc this
a le type Foo*
, non const Foo*
, dans le constructeur.
Par conséquent, un comportement indéfini se produit même s'il n'y a pas de constructions manifestement dangereuses dans ce programme.
Accès à un membre inexistant via un pointeur sur un membre
Lorsque vous accédez à un membre non statique d'un objet via un pointeur sur membre, si l'objet ne contient pas réellement le membre indiqué par le pointeur, le comportement est indéfini. (Un tel pointeur vers membre peut être obtenu via static_cast
.)
struct Base { int x; };
struct Derived : Base { int y; };
int Derived::*pdy = &Derived::y;
int Base::*pby = static_cast<int Base::*>(pdy);
Base* b1 = new Derived;
b1->*pby = 42; // ok; sets y in Derived object to 42
Base* b2 = new Base;
b2->*pby = 42; // undefined; there is no y member in Base
Conversion de base en base invalide pour les pointeurs vers les membres
Lorsque static_cast
est utilisé pour convertir TD::*
en TB::*
, le membre désigné doit appartenir à une classe qui est une classe de base ou une classe dérivée de B
Sinon, le comportement n'est pas défini. Voir Dérivé à la conversion de base pour les pointeurs vers les membres
Arithmétique de pointeur invalide
Les utilisations suivantes de l'arithmétique de pointeur provoquent un comportement indéfini:
Ajout ou soustraction d'un entier, si le résultat n'appartient pas au même objet tableau que l'opérande du pointeur. (Ici, l'élément un passé est considéré comme appartenant toujours au tableau.)
int a[10]; int* p1 = &a[5]; int* p2 = p1 + 4; // ok; p2 points to a[9] int* p3 = p1 + 5; // ok; p2 points to one past the end of a int* p4 = p1 + 6; // UB int* p5 = p1 - 5; // ok; p2 points to a[0] int* p6 = p1 - 6; // UB int* p7 = p3 - 5; // ok; p7 points to a[5]
Soustraction de deux pointeurs s'ils n'appartiennent pas tous deux au même objet tableau. (Encore une fois, l'élément un passé après la fin est considéré comme appartenant au tableau.) L'exception est que deux pointeurs nuls peuvent être soustraits, ce qui donne 0.
int a[10]; int b[10]; int *p1 = &a[8], *p2 = &a[3]; int d1 = p1 - p2; // yields 5 int *p3 = p1 + 2; // ok; p3 points to one past the end of a int d2 = p3 - p2; // yields 7 int *p4 = &b[0]; int d3 = p4 - p1; // UB
Soustraction de deux pointeurs si le résultat déborde
std::ptrdiff_t
.Toute arithmétique de pointeur où le type de pointe de l'un des opérandes ne correspond pas au type dynamique de l'objet pointé (en ignorant la qualification cv). Selon la norme, "[en] particulier, un pointeur vers une classe de base ne peut pas être utilisé pour l'arithmétique du pointeur lorsque le tableau contient des objets d'un type de classe dérivé."
struct Base { int x; }; struct Derived : Base { int y; }; Derived a[10]; Base* p1 = &a[1]; // ok Base* p2 = p1 + 1; // UB; p1 points to Derived Base* p3 = p1 - 1; // likewise Base* p4 = &a[2]; // ok auto p5 = p4 - p1; // UB; p4 and p1 point to Derived const Derived* p6 = &a[1]; const Derived* p7 = p6 + 1; // ok; cv-qualifiers don't matter
Déplacement par un nombre de postes invalide
Pour l'opérateur de quart intégré, l'opérande droit doit être non négatif et strictement inférieur à la largeur de bit de l'opérande gauche promu. Sinon, le comportement est indéfini.
const int a = 42;
const int b = a << -1; // UB
const int c = a << 0; // ok
const int d = a << 32; // UB if int is 32 bits or less
const int e = a >> 32; // also UB if int is 32 bits or less
const signed char f = 'x';
const int g = f << 10; // ok even if signed char is 10 bits or less;
// int must be at least 16 bits
Retourner d'une fonction [[noreturn]]
Exemple du standard, [dcl.attr.noreturn]:
[[ noreturn ]] void f() {
throw "error"; // OK
}
[[ noreturn ]] void q(int i) { // behavior is undefined if called with an argument <= 0
if (i > 0)
throw "positive";
}
Détruire un objet qui a déjà été détruit
Dans cet exemple, un destructeur est explicitement appelé pour un objet qui sera ultérieurement détruit automatiquement.
struct S {
~S() { std::cout << "destroying S\n"; }
};
int main() {
S s;
s.~S();
} // UB: s destroyed a second time here
Un problème similaire se produit lorsqu'un std::unique_ptr<T>
est dirigé vers un T
avec une durée de stockage automatique ou statique.
void f(std::unique_ptr<S> p);
int main() {
S s;
std::unique_ptr<S> p(&s);
f(std::move(p)); // s destroyed upon return from f
} // UB: s destroyed
Une autre façon de détruire un objet à deux reprises consiste à faire en sorte que deux shared_ptr
gèrent l'objet sans en partager la propriété.
void f(std::shared_ptr<S> p1, std::shared_ptr<S> p2);
int main() {
S* p = new S;
// I want to pass the same object twice...
std::shared_ptr<S> sp1(p);
std::shared_ptr<S> sp2(p);
f(sp1, sp2);
} // UB: both sp1 and sp2 will destroy s separately
// NB: this is correct:
// std::shared_ptr<S> sp(p);
// f(sp, sp);
Récursion du modèle infini
Exemple de la norme, [temp.inst] / 17:
template<class T> class X {
X<T>* p; // OK
X<T*> a; // implicit generation of X<T> requires
// the implicit instantiation of X<T*> which requires
// the implicit instantiation of X<T**> which ...
};