Quels sont tous les comportements non définis communs qu'un programmeur C ++ devrait connaître? [fermé]

201

Quels sont tous les comportements non définis communs qu'un programmeur C ++ devrait connaître?

Dites, comme:

a[i] = i++;

yesraaj
la source
3
Êtes-vous sûr. Cela semble bien défini.
Martin York,
17
6.2.2 Ordre d'évaluation [expr.evaluation] dans le langage de programmation C ++ le dit. Je n'ai pas d'autre référence
yesraaj
4
Il a raison .. vient de regarder 6.2.2 dans le langage de programmation C ++ et il dit que v [i] = i ++ n'est pas défini
dancavallaro
4
J'imagine que le comiler fait exécuter l'i ++ avant ou après le calcul de l'emplacement mémoire de v [i]. bien sûr, je vais toujours y être affecté. mais il pourrait écrire dans v [i] ou v [i + 1] selon l'ordre des opérations ..
Evan Teran
2
Tout ce que le langage de programmation C ++ dit est "L'ordre des opérations des sous-expressions dans une expression n'est pas défini. En particulier, vous ne pouvez pas supposer que l'expression est évaluée de gauche à droite."
dancavallaro

Réponses:

233

Aiguille

  • Déréférencer un NULLpointeur
  • Déréférencer un pointeur renvoyé par une "nouvelle" allocation de taille zéro
  • Utilisation de pointeurs vers des objets dont la durée de vie est terminée (par exemple, empiler des objets alloués ou des objets supprimés)
  • Déréférencer un pointeur qui n'a pas encore été définitivement initialisé
  • Exécution d'une arithmétique de pointeur qui donne un résultat en dehors des limites (au-dessus ou en dessous) d'un tableau.
  • Déréférencer le pointeur à un emplacement au-delà de la fin d'un tableau.
  • Conversion de pointeurs en objets de types incompatibles
  • Utilisation memcpypour copier des tampons qui se chevauchent .

Débordements de tampon

  • Lecture ou écriture sur un objet ou un tableau avec un décalage négatif ou supérieur à la taille de cet objet (débordement de pile / tas)

Débordements entiers

  • Débordement d'entier signé
  • Évaluation d'une expression qui n'est pas définie mathématiquement
  • Décalage à gauche des valeurs par un montant négatif (les décalages à droite par des montants négatifs sont définis par l'implémentation)
  • Décaler les valeurs d'une quantité supérieure ou égale au nombre de bits du nombre (par exemple, int64_t i = 1; i <<= 72n'est pas défini)

Types, distribution et const

  • Transformer une valeur numérique en une valeur qui ne peut pas être représentée par le type cible (directement ou via static_cast)
  • Utiliser une variable automatique avant qu'elle ne soit définitivement attribuée (par exemple, int i; i++; cout << i;)
  • Utilisation de la valeur de tout objet de type autre que volatileousig_atomic_t à la réception d'un signal
  • Tentative de modification d'un littéral de chaîne ou de tout autre objet const au cours de sa durée de vie
  • Concaténation d'un littéral de chaîne étroite avec une chaîne large pendant le prétraitement

Fonction et modèle

  • Ne pas retourner une valeur à partir d'une fonction de retour de valeur (directement ou en sortant d'un bloc try)
  • Plusieurs définitions différentes pour la même entité (classe, modèle, énumération, fonction inline, fonction membre statique, etc.)
  • Récursion infinie dans l'instanciation de modèles
  • Appel d'une fonction à l'aide de différents paramètres ou liaison aux paramètres et liaison que la fonction est définie comme utilisant.

OOP

  • Destructions en cascade d'objets avec une durée de stockage statique
  • Résultat de l'affectation à des objets se chevauchant partiellement
  • Rentrée récursive d'une fonction lors de l'initialisation de ses objets statiques
  • Faire des appels de fonctions virtuelles aux fonctions virtuelles pures d'un objet à partir de son constructeur ou destructeur
  • Se référant à des éléments non statiques d'objets qui n'ont pas été construits ou qui ont déjà été détruits

Fichier source et prétraitement

  • Un fichier source non vide qui ne se termine pas par une nouvelle ligne ou se termine par une barre oblique inverse (avant C ++ 11)
  • Une barre oblique inverse suivie d'un caractère qui ne fait pas partie des codes d'échappement spécifiés dans une constante de caractère ou de chaîne (ceci est défini par l'implémentation en C ++ 11).
  • Dépassement des limites d'implémentation (nombre de blocs imbriqués, nombre de fonctions dans un programme, espace de pile disponible ...)
  • Valeurs numériques du préprocesseur qui ne peuvent pas être représentées par un long int
  • Directive de prétraitement sur le côté gauche d'une définition de macro de type fonction
  • Génération dynamique du jeton défini dans une #ifexpression

Être classé

  • Appel de exit lors de la destruction d'un programme avec une durée de stockage statique
Diomidis Spinellis
la source
Hm ... NaN (x / 0) et Infinity (0/0) étaient couverts par l'IEE 754, si C ++ a été conçu plus tard, pourquoi enregistre-t-il x / 0 comme non défini?
new123456
Re: "Une barre oblique inverse suivie d'un caractère qui ne fait pas partie des codes d'échappement spécifiés dans une constante de caractère ou de chaîne." C'est UB en C89 (§3.1.3.4) et C ++ 03 (qui intègre C89), mais pas en C99. C99 dit que "le résultat n'est pas un jeton et un diagnostic est requis" (§6.4.4.4). Vraisemblablement C ++ 0x (qui intègre C89) sera le même.
Adam Rosenfield
1
La norme C99 a une liste de comportements non définis en annexe J.2. Il faudrait du travail pour adapter cette liste au C ++. Vous devez modifier les références aux clauses C ++ correctes plutôt qu'aux clauses C99, supprimer tout ce qui n'est pas pertinent et vérifier également si toutes ces choses ne sont pas vraiment définies en C ++ ainsi qu'en C. Mais cela fournit un début.
Steve Jessop
1
@ new123456 - toutes les unités à virgule flottante ne sont pas compatibles IEE754. Si C ++ exigeait la conformité IEE754, les compilateurs devraient tester et gérer le cas où le RHS est nul via une vérification explicite. En rendant le comportement non défini, le compilateur peut éviter cette surcharge en disant "si vous utilisez un FPU non IEE754, vous n'obtiendrez pas le comportement du FPU IEEE754".
SecurityMatt
1
"Évaluation d'une expression dont le résultat ne se trouve pas dans la plage des types correspondants" .... le débordement d'entier est bien défini pour les types intégraux UNSIGNED, mais pas pour les types signés.
nacitar sevaht
31

L'ordre dans lequel les paramètres de fonction sont évalués est un comportement non spécifié . (Cela ne fera pas planter votre programme, exploser ou commander une pizza ... contrairement au comportement non défini .)

La seule exigence est que tous les paramètres doivent être entièrement évalués avant l'appel de la fonction.


Ce:

// The simple obvious one.
callFunc(getA(),getB());

Peut être équivalent à ceci:

int a = getA();
int b = getB();
callFunc(a,b);

Ou ca:

int b = getB();
int a = getA();
callFunc(a,b);

Cela peut être soit; c'est au compilateur. Le résultat peut être important, selon les effets secondaires.

Martin York
la source
23
L'ordre n'est pas spécifié, pas indéfini.
Rob Kennedy
1
Je déteste celui-ci :) J'ai perdu une journée de travail après avoir retrouvé l'un de ces cas ... de toute façon, j'ai appris ma leçon et je ne suis pas tombé à nouveau heureusement
Robert Gould
2
@Rob: Je discuterais avec vous du changement de sens ici, mais je sais que le comité des normes est très pointilleux sur la définition exacte de ces deux mots. Je vais donc juste le changer :-)
Martin York
2
J'ai eu de la chance sur celui-ci. J'en ai été mordu quand j'étais au collège et j'ai eu un professeur qui a jeté un coup d'œil et m'a dit mon problème en environ 5 secondes. Sans dire combien de temps j'aurais perdu le débogage autrement.
Bill the Lizard
27

Le compilateur est libre de réorganiser les parties d'évaluation d'une expression (en supposant que la signification est inchangée).

De la question d'origine:

a[i] = i++;

// This expression has three parts:
(a) a[i]
(b) i++
(c) Assign (b) to (a)

// (c) is guaranteed to happen after (a) and (b)
// But (a) and (b) can be done in either order.
// See n2521 Section 5.17
// (b) increments i but returns the original value.
// See n2521 Section 5.2.6
// Thus this expression can be written as:

int rhs  = i++;
int lhs& = a[i];
lhs = rhs;

// or
int lhs& = a[i];
int rhs  = i++;
lhs = rhs;

Verrouillage à double contrôle. Et une simple erreur à commettre.

A* a = new A("plop");

// Looks simple enough.
// But this can be split into three parts.
(a) allocate Memory
(b) Call constructor
(c) Assign value to 'a'

// No problem here:
// The compiler is allowed to do this:
(a) allocate Memory
(c) Assign value to 'a'
(b) Call constructor.
// This is because the whole thing is between two sequence points.

// So what is the big deal.
// Simple Double checked lock. (I know there are many other problems with this).
if (a == null) // (Point B)
{
    Lock   lock(mutex);
    if (a == null)
    {
        a = new A("Plop");  // (Point A).
    }
}
a->doStuff();

// Think of this situation.
// Thread 1: Reaches point A. Executes (a)(c)
// Thread 1: Is about to do (b) and gets unscheduled.
// Thread 2: Reaches point B. It can now skip the if block
//           Remember (c) has been done thus 'a' is not NULL.
//           But the memory has not been initialized.
//           Thread 2 now executes doStuff() on an uninitialized variable.

// The solution to this problem is to move the assignment of 'a'
// To the other side of the sequence point.
if (a == null) // (Point B)
{
    Lock   lock(mutex);
    if (a == null)
    {
        A* tmp = new A("Plop");  // (Point A).
        a = tmp;
    }
}
a->doStuff();

// Of course there are still other problems because of C++ support for
// threads. But hopefully these are addresses in the next standard.
Martin York
la source
qu'entend-on par point de séquence?
yesraaj
1
Ooh ... c'est méchant, surtout depuis que j'ai vu la structure exacte recommandée en Java
Tom
Notez que certains compilateurs définissent le comportement dans cette situation. Dans VC ++ 2005+, par exemple, si a est volatile, les bariers de mémoire nécessaires sont configurés pour empêcher la réorganisation des instructions afin que le verrouillage à double vérification fonctionne.
Eclipse le
Martin York: <i> // (c) est garanti de se produire après (a) et (b) </i> Est-ce? Certes, dans cet exemple particulier, le seul scénario où cela pourrait être important serait si «i» était une variable volatile mappée à un registre matériel, et un [i] (ancienne valeur de «i») lui était alias, mais y a-t-il un garantir que l'incrément se produira avant un point de séquence?
supercat
5

Mon préféré est "Récursion infinie dans l'instanciation des modèles" car je crois que c'est le seul où le comportement indéfini se produit au moment de la compilation.

Daniel Earwicker
la source
Fait avant, mais je ne vois pas comment son indéfini. C'est assez évident que vous fassiez une récursion infinie après coup.
Robert Gould
Le problème est que le compilateur ne peut pas examiner votre code et décider précisément s'il souffrira d'une récursion infinie ou non. C'est un exemple du problème de l'arrêt. Voir: stackoverflow.com/questions/235984/…
Daniel Earwicker
Ouais, c'est définitivement un problème d'arrêt
Robert Gould
cela a fait planter mon système à cause d'un échange causé par trop peu de mémoire.
Johannes Schaub - litb
2
Les constantes de préprocesseur qui ne rentrent pas dans un int sont également du temps de compilation.
Joshua
5

Affectation à une constante après dénudage en constutilisant const_cast<>:

const int i = 10; 
int *p =  const_cast<int*>( &i );
*p = 1234; //Undefined
yesraaj
la source
5

Outre le comportement non défini , il existe également un comportement défini par l' implémentation tout aussi désagréable .

Un comportement non défini se produit lorsqu'un programme fait quelque chose dont le résultat n'est pas spécifié par la norme.

Le comportement défini par l'implémentation est une action d'un programme dont le résultat n'est pas défini par la norme, mais que l'implémentation doit documenter. Un exemple est "Littéraux de caractères multi-octets", de la question Stack Overflow Y a-t-il un compilateur C qui ne parvient pas à le compiler? .

Le comportement défini par l'implémentation ne vous mord que lorsque vous démarrez le portage (mais la mise à niveau vers une nouvelle version du compilateur est également un portage!)

Constantin
la source
4

Les variables ne peuvent être mises à jour qu'une seule fois dans une expression (techniquement une fois entre des points de séquence).

int i =1;
i = ++i;

// Undefined. Assignment to 'i' twice in the same expression.
Martin York
la source
Enfait au moins une fois entre deux points de séquence.
Prasoon Saurav
2
@Prasoon: Je pense que vous vouliez dire: au plus une fois entre deux points de séquence. :-)
Nawaz
3

Une compréhension de base des différentes limites environnementales. La liste complète se trouve à la section 5.2.4.1 de la spécification C. Voici quelques-uns;

  • 127 paramètres dans une dé fi nition de fonction
  • 127 arguments dans un appel de fonction
  • 127 paramètres dans une définition de macro
  • 127 arguments en une seule invocation de macro
  • 4095 caractères dans une ligne source logique
  • 4095 caractères dans un littéral de chaîne de caractères ou un littéral de chaîne large (après concaténation)
  • 65535 octets dans un objet (dans un environnement hébergé uniquement)
  • 15 niveaux de nidification pour les fichiers #inclus
  • 1023 étiquettes de cas pour une instruction switch (à l'exclusion de celles des instructions switch imbriquées)

J'étais en fait un peu surpris de la limite de 1023 étiquettes de cas pour une instruction switch, je peux prévoir que le dépassement pour le code / lex / analyseurs généré assez facilement.

Si ces limites sont dépassées, vous avez un comportement indéfini (plantages, failles de sécurité, etc ...).

Bon, je sais que cela vient de la spécification C, mais C ++ partage ces supports de base.

RandomNickName42
la source
9
Si vous atteignez ces limites, vous avez plus de problèmes qu'un comportement indéfini.
nouveau123456
Vous pourriez FACILEMENT dépasser 65535 octets dans un objet, tel qu'un STD :: vector
Demi
2

Permet memcpyde copier entre des régions de mémoire qui se chevauchent. Par exemple:

char a[256] = {};
memcpy(a, a, sizeof(a));

Le comportement n'est pas défini selon la norme C, qui est subsumée par la norme C ++ 03.

7.21.2.1 La fonction memcpy

Synopsis

1 / #include void * memcpy (void * restreindre s1, const void * restreindre s2, size_t n);

La description

2 / La fonction memcpy copie n caractères de l'objet pointé par s2 dans l'objet pointé par s1. Si la copie a lieu entre des objets qui se chevauchent, le comportement n'est pas défini. Renvoie 3 La fonction memcpy renvoie la valeur de s1.

7.21.2.2 La fonction memmove

Synopsis

1 #include void * memmove (void * s1, const void * s2, size_t n);

La description

2 La fonction memmove copie n caractères de l'objet pointé par s2 dans l'objet pointé par s1. La copie se déroule comme si les n caractères de l'objet pointé par s2 sont d'abord copiés dans un tableau temporaire de n caractères qui ne chevauchent pas les objets pointés par s1 et s2, puis les n caractères du tableau temporaire sont copiés dans l'objet pointé par s1. Retour

3 La fonction memmove renvoie la valeur de s1.

John Dibling
la source
2

Le seul type pour lequel C ++ garantit une taille est char. Et la taille est 1. La taille de tous les autres types dépend de la plate-forme.

JaredPar
la source
N'est-ce pas à cela que sert <cstdint>? Il définit des types tels que uint16_6 et cetera.
Jasper Bekkers
Oui, mais la taille de la plupart des types, disons longue, n'est pas bien définie.
JaredPar
cstdint ne fait pas encore partie de la norme c ++ actuelle. voir boost / stdint.hpp pour une solution actuellement portable.
Evan Teran
Ce n'est pas un comportement indéfini. La norme indique que la plate-forme conforme définit les tailles, plutôt que la norme les définissant.
Daniel Earwicker
1
@JaredPar: C'est un article complexe avec beaucoup de fils de conversation, donc je l'ai résumé ici . La ligne de fond est la suivante: "5. Pour représenter -2147483647 et +2147483647 en binaire, vous avez besoin de 32 bits."
John Dibling
2

Les objets de niveau espace de noms dans des unités de compilation différentes ne doivent jamais dépendre les uns des autres pour l'initialisation, car leur ordre d'initialisation n'est pas défini.

yesraaj
la source