Je voudrais préparer un petit outil éducatif pour SO qui devrait aider les programmeurs débutants (et intermédiaires) à reconnaître et à contester leurs hypothèses injustifiées en C, C ++ et leurs plates-formes.
Exemples:
- "les entiers s'enroulent"
- "tout le monde a ASCII"
- "Je peux stocker un pointeur de fonction dans un vide *"
J'ai pensé qu'un petit programme de test pourrait être exécuté sur différentes plates-formes, qui exécute les hypothèses «plausibles» qui sont, d'après notre expérience en SO, généralement faites par de nombreux développeurs grand public inexpérimentés / semi-expérimentés et enregistrent les façons dont ils se cassent sur diverses machines.
Le but de ceci n'est pas de prouver qu'il est «sûr» de faire quelque chose (ce qui serait impossible à faire, les tests ne prouvent que quoi que ce soit s'ils cassent), mais plutôt de démontrer à l'individu le plus incompréhensible comment l'expression la plus discrète break sur une machine différente, si elle a un comportement non défini ou défini par l'implémentation. .
Pour y parvenir, je voudrais vous demander:
- Comment cette idée peut-elle être améliorée?
- Quels tests seraient bons et à quoi devraient-ils ressembler?
- Souhaitez-vous exécuter les tests sur les plates-formes sur lesquelles vous pouvez mettre la main et publier les résultats, de sorte que nous nous retrouvions avec une base de données de plates-formes, en quoi elles diffèrent et pourquoi cette différence est autorisée?
Voici la version actuelle du jouet de test:
#include <stdio.h>
#include <limits.h>
#include <stdlib.h>
#include <stddef.h>
int count=0;
int total=0;
void expect(const char *info, const char *expr)
{
printf("..%s\n but '%s' is false.\n",info,expr);
fflush(stdout);
count++;
}
#define EXPECT(INFO,EXPR) if (total++,!(EXPR)) expect(INFO,#EXPR)
/* stack check..How can I do this better? */
ptrdiff_t check_grow(int k, int *p)
{
if (p==0) p=&k;
if (k==0) return &k-p;
else return check_grow(k-1,p);
}
#define BITS_PER_INT (sizeof(int)*CHAR_BIT)
int bits_per_int=BITS_PER_INT;
int int_max=INT_MAX;
int int_min=INT_MIN;
/* for 21 - left to right */
int ltr_result=0;
unsigned ltr_fun(int k)
{
ltr_result=ltr_result*10+k;
return 1;
}
int main()
{
printf("We like to think that:\n");
/* characters */
EXPECT("00 we have ASCII",('A'==65));
EXPECT("01 A-Z is in a block",('Z'-'A')+1==26);
EXPECT("02 big letters come before small letters",('A'<'a'));
EXPECT("03 a char is 8 bits",CHAR_BIT==8);
EXPECT("04 a char is signed",CHAR_MIN==SCHAR_MIN);
/* integers */
EXPECT("05 int has the size of pointers",sizeof(int)==sizeof(void*));
/* not true for Windows-64 */
EXPECT("05a long has at least the size of pointers",sizeof(long)>=sizeof(void*));
EXPECT("06 integers are 2-complement and wrap around",(int_max+1)==(int_min));
EXPECT("07 integers are 2-complement and *always* wrap around",(INT_MAX+1)==(INT_MIN));
EXPECT("08 overshifting is okay",(1<<bits_per_int)==0);
EXPECT("09 overshifting is *always* okay",(1<<BITS_PER_INT)==0);
{
int t;
EXPECT("09a minus shifts backwards",(t=-1,(15<<t)==7));
}
/* pointers */
/* Suggested by jalf */
EXPECT("10 void* can store function pointers",sizeof(void*)>=sizeof(void(*)()));
/* execution */
EXPECT("11 Detecting how the stack grows is easy",check_grow(5,0)!=0);
EXPECT("12 the stack grows downwards",check_grow(5,0)<0);
{
int t;
/* suggested by jk */
EXPECT("13 The smallest bits always come first",(t=0x1234,0x34==*(char*)&t));
}
{
/* Suggested by S.Lott */
int a[2]={0,0};
int i=0;
EXPECT("14 i++ is strictly left to right",(i=0,a[i++]=i,a[0]==1));
}
{
struct {
char c;
int i;
} char_int;
EXPECT("15 structs are packed",sizeof(char_int)==(sizeof(char)+sizeof(int)));
}
{
EXPECT("16 malloc()=NULL means out of memory",(malloc(0)!=NULL));
}
/* suggested by David Thornley */
EXPECT("17 size_t is unsigned int",sizeof(size_t)==sizeof(unsigned int));
/* this is true for C99, but not for C90. */
EXPECT("18 a%b has the same sign as a",((-10%3)==-1) && ((10%-3)==1));
/* suggested by nos */
EXPECT("19-1 char<short",sizeof(char)<sizeof(short));
EXPECT("19-2 short<int",sizeof(short)<sizeof(int));
EXPECT("19-3 int<long",sizeof(int)<sizeof(long));
EXPECT("20 ptrdiff_t and size_t have the same size",(sizeof(ptrdiff_t)==sizeof(size_t)));
#if 0
{
/* suggested by R. */
/* this crashed on TC 3.0++, compact. */
char buf[10];
EXPECT("21 You can use snprintf to append a string",
(snprintf(buf,10,"OK"),snprintf(buf,10,"%s!!",buf),strcmp(buf,"OK!!")==0));
}
#endif
EXPECT("21 Evaluation is left to right",
(ltr_fun(1)*ltr_fun(2)*ltr_fun(3)*ltr_fun(4),ltr_result==1234));
{
#ifdef __STDC_IEC_559__
int STDC_IEC_559_is_defined=1;
#else
/* This either means, there is no FP support
*or* the compiler is not C99 enough to define __STDC_IEC_559__
*or* the FP support is not IEEE compliant. */
int STDC_IEC_559_is_defined=0;
#endif
EXPECT("22 floating point is always IEEE",STDC_IEC_559_is_defined);
}
printf("From what I can say with my puny test cases, you are %d%% mainstream\n",100-(100*count)/total);
return 0;
}
Oh, et j'ai créé ce wiki communautaire dès le début parce que je pensais que les gens voulaient éditer mon discours quand ils liraient ceci.
MISE À JOUR Merci pour votre contribution. J'ai ajouté quelques cas à partir de vos réponses et je vais voir si je peux configurer un github pour cela, comme Greg l'a suggéré.
MISE À JOUR : J'ai créé un dépôt github pour cela, le fichier est "gotcha.c":
Veuillez répondre ici avec des correctifs ou de nouvelles idées, afin qu'elles puissent être discutées ou clarifiées ici. Je vais alors les fusionner dans gotcha.c.
dlsym()
renvoie un void * mais est destiné à la fois aux pointeurs de données et de fonction. Par conséquent, il n'est peut-être pas si grave de dépendre de cela.Réponses:
L'ordre d'évaluation des sous-expressions, y compris
+
,-
,=
,*
,/
), à l'exception de:&&
et||
),?:
), et,
)n'est pas spécifié
Par exemple
la source
boost::spirit
)+
opérateur n'est pas spécifié (les rédacteurs du compilateur n'ont pas besoin de documenter le comportement). Il ne viole aucune règle de point de séquence en tant que telle.sdcc 29.7 / ucSim / Z80
printf plante. "O_O"
gcc 4.4@x86_64-suse-linux
gcc 4.4@x86_64-suse-linux (-O2)
clang 2.7@x86_64-suse-linux
open64 4.2.3@x86_64-suse-linux
intel 11.1@x86_64-suse-linux
Turbo C ++ / DOS / Petite mémoire
Turbo C ++ / DOS / Mémoire moyenne
Turbo C ++ / DOS / Mémoire compacte
cl65 @ Commodore PET (émulateur vice)
Je les mettrai à jour plus tard:
Borland C ++ Builder 6.0 sur Windows XP
Visual Studio Express 2010 C ++ CLR, Windows 7 64 bits
(doit être compilé en C ++ car le compilateur CLR ne prend pas en charge le C pur)
MINGW64 (pré-version gcc-4.5.2)
- http://mingw-w64.sourceforge.net/
Windows 64 bits utilise le modèle LLP64: les deux
int
etlong
sont définis comme 32 bits, ce qui signifie qu'aucun n'est assez long pour un pointeur.avr-gcc 4.3.2 / ATmega168 (Arduino Diecimila)
Les hypothèses qui ont échoué sont:
L'Atmega168 a un PC 16 bits, mais le code et les données sont dans des espaces d'adressage séparés. Les plus grands Atmegas ont un PC 22 bits !.
gcc 4.2.1 sur MacOSX 10.6, compilé avec -arch ppc
la source
sizeof(void*)>=sizeof(void(*)())
serait plus pertinent que ==. Tout ce qui nous importe, c'est "pouvons-nous stocker un pointeur de fonction dans un pointeur vide", donc l'hypothèse que vous devez tester est de savoir si avoid*
est au moins aussi grand qu'un pointeur de fonction.sizeof(void*)>=sizeof(void(*)())
- voir opengroup.org/onlinepubs/009695399/functions/dlsym.htmlIl y a longtemps, j'enseignais le C à partir d'un manuel qui avait
comme exemple de question. Cela a échoué pour un étudiant, parce que les
sizeof
valeurs de typesize_t
, nonint
,int
sur cette implémentation étaient 16 bits etsize_t
32, et c'était big-endian. (La plate-forme était Lightspeed C sur Macintosh 680x0. J'ai dit que c'était il y a longtemps.)la source
unsigned long long
là. Ajouté en tant que test 17.z
modificateur poursize_t
les entiers dimensionnés etlong long
n'est pas pris en charge sur certaines plates-formes également. Il n'y a donc pas de moyen portable sûr de formater ou de mouler la taille imprimée d'un objet.Vous devez inclure les hypothèses
++
et les--
hypothèses émises par les gens.Par exemple, est syntaxiquement légale, mais produit des résultats variables en fonction de trop de choses à raisonner.
Toute instruction qui a
++
(ou--
) et une variable qui se produit plus d'une fois est un problème.la source
Très intéressant!
D'autres choses auxquelles je peux penser pourraient être utiles à vérifier:
les pointeurs de fonction et les pointeurs de données existent-ils dans le même espace d'adressage? (Ruptures dans les machines d'architecture de Harvard comme le petit mode DOS. Je ne sais pas comment vous le testeriez, cependant.)
si vous prenez un pointeur de données NULL et le transtypez dans le type entier approprié, a-t-il la valeur numérique 0? (Rupture sur certaines machines vraiment anciennes --- voir http://c-faq.com/null/machexamp.html .) Idem avec le pointeur de fonction. En outre, il peut s'agir de valeurs différentes.
est-ce que l'incrémentation d'un pointeur au-delà de la fin de son objet de stockage correspondant, puis en arrière, produit des résultats raisonnables? (Je ne connais pas de machines sur lesquelles cela fonctionne réellement, mais je pense que la spécification C ne vous permet même pas de penser à des pointeurs qui ne pointent ni vers (a) le contenu d'un tableau ni (b) l'élément immédiatement après le tableau ou (c) NULL. Voir http://c-faq.com/aryptr/non0based.html .)
la comparaison de deux pointeurs vers différents objets de stockage avec <et> produit-elle des résultats cohérents? (Je peux imaginer cette rupture sur des machines exotiques basées sur des segments; la spécification interdit de telles comparaisons, de sorte que le compilateur aurait le droit de comparer uniquement la partie décalée du pointeur, et non la partie segment.)
Hmm. Je vais essayer de penser à un peu plus.
Edit: Ajout de quelques liens de clarification vers l'excellente FAQ C.
la source
Je pense que vous devriez faire un effort pour faire la distinction entre deux classes très différentes d'hypothèses «incorrectes». Une bonne moitié (décalage à droite et extension de signe, encodage compatible ASCII, la mémoire est linéaire, les pointeurs de données et de fonction sont compatibles, etc.) sont des hypothèses assez raisonnables à faire pour la plupart des codeurs C, et pourraient même être incluses dans le standard si C était en cours de conception aujourd'hui et si nous n'avions pas de droits acquis sur les déchets IBM hérités. L'autre moitié (choses liées à l'alias de la mémoire, comportement des fonctions de la bibliothèque lorsque la mémoire d'entrée et de sortie se chevauchent, hypothèses 32 bits comme celle des pointeurs
int
ou que vous pouvez utilisermalloc
sans prototype, cette convention d'appel est identique pour les fonctions variadiques et non variadiques, ...) soit en conflit avec les optimisations que les compilateurs modernes veulent effectuer, soit avec la migration vers des machines 64 bits ou d'autres nouvelles technologies.la source
malloc
sans prototype signifie ne pas inclure<stdlib.h>
, ce qui entraîne lamalloc
valeur par défautint malloc(int)
, un non-non si vous souhaitez prendre en charge le 64 bits.<stdlib.h>
tant que vous incluez un autre en-tête qui définitsize_t
et que vous déclarez ensuitemalloc
avec un prototype correct vous-même.En voici une amusante: quel est le problème avec cette fonction?
[Réponse (rot13): Inevnqvp nethzragf borl gur byq X&E cebzbgvba ehyrf, juvpu zrnaf lbh pnaabg hfr 'sybng' (be 'pune' be 'fubeg') va in_net! Naq gur pbzcvyre vf erdhverq abg gb gerng guvf nf n pbzcvyr-gvzr reebe. (TPP qbrf rzvg n jneavat, gubhtu.)]
la source
Un autre concerne le mode texte dans
fopen
. La plupart des programmeurs supposent que texte et binaire sont identiques (Unix) ou que le mode texte ajoute des\r
caractères (Windows). Mais C a été porté sur des systèmes qui utilisent des enregistrements à largeur fixe, sur lesquelsfputc('\n', file)
sur un fichier texte signifie ajouter des espaces ou quelque chose jusqu'à ce que la taille du fichier soit un multiple de la longueur de l'enregistrement.Et voici mes résultats:
gcc (Ubuntu 4.4.3-4ubuntu5) 4.4.3 sur x86-64
la source
pow(2, n)
avec des opérations sur les bits.Certains d'entre eux ne peuvent pas être facilement testés depuis l'intérieur de C car le programme est susceptible de planter sur les implémentations où l'hypothèse ne tient pas.
"Il est normal de faire quoi que ce soit avec une variable à valeur de pointeur. Elle n'a besoin de contenir une valeur de pointeur valide que si vous la déréférencer."
Idem pour les types à virgule flottante et intégrale (autres que
unsigned char
), qui sont autorisés à avoir des représentations d'interruption."Les calculs entiers se terminent. Donc, ce programme imprime un grand entier négatif."
(C89 uniquement.) "Il est normal de tomber à la fin de
main
."la source
gcc -ftrapv -O
, la sortie estWe like to think that:
suivie deAborted
main
sans valeur: le programme est correct mais renvoie un état de terminaison indéfini (C89 §2.1.2.2). Avec de nombreuses implémentations (telles que gcc et les anciens compilateurs Unix), vous obtenez tout ce qui se trouvait dans un certain registre à ce stade. Le programme fonctionne généralement jusqu'à ce qu'il soit utilisé dans un makefile ou un autre environnement qui vérifie l'état de fin.Eh bien, les hypothèses classiques de portabilité qui ne sont pas encore signifiées sont
la source
short
valeur fedcab9876543210 (soit 16 chiffres binaires) comme les deux octets 0248ace et fdb97531.Erreurs de discrétisation dues à la représentation en virgule flottante. Par exemple, si vous utilisez la formule standard pour résoudre des équations quadratiques, ou des différences finies pour approcher les dérivées, ou la formule standard pour calculer les variances, la précision sera perdue en raison du calcul des différences entre des nombres similaires. L'algorithme de Gauß pour résoudre des systèmes linéaires est mauvais car les erreurs d'arrondi s'accumulent, donc on utilise la décomposition QR ou LU, la décomposition de Cholesky, SVD, etc. L'ajout de nombres à virgule flottante n'est pas associatif. Il existe des valeurs dénormales, infinies et NaN. a + b - a ≠ b .
Chaînes: différence entre les caractères, les points de code et les unités de code. Comment Unicode est implémenté sur les différents systèmes d'exploitation; Encodages Unicode. L'ouverture d'un fichier avec un nom de fichier Unicode arbitraire n'est pas possible avec C ++ de manière portable.
Conditions de course, même sans thread: si vous testez si un fichier existe, le résultat peut devenir invalide à tout moment.
ERROR_SUCCESS
= 0la source
Incluez une vérification des tailles entières. La plupart des gens supposent qu'un int est plus grand qu'un court est plus grand qu'un caractère. Cependant, tout cela peut être faux:
sizeof(char) < sizeof(int); sizeof(short) < sizeof(int); sizeof(char) < sizeof(short)
Ce code peut échouer (se bloque lors d'un accès non aligné)
la source
int *p = (int*)&buf[1];
en C ++, les gens s'attendent à ce que cela fonctionne aussi.sizeof(char) < sizeof(int)
est requis. Par exemple, fgetc () renvoie la valeur du caractère sous la forme d'un caractère non signé converti en int, ouEOF
qui est une valeur négative.unsigned char
peut ne pas avoir de bits de remplissage, donc la seule façon de le faire est de rendre int plus grand que char. En outre, (la plupart des versions de) la spécification C exigent que toute valeur comprise entre -32767..32767 puisse être stockée dans un int.Quelques informations sur les types de données intégrés:
char
etsigned char
sont en fait deux types distincts (contrairementint
etsigned int
qui font référence au même type entier signé).-3/5
pourrait retourner0
ou-1
. Arrondir vers zéro au cas où un opérande serait négatif n'est garanti que dans C99 vers le haut et C ++ 0x vers le haut.int
a au moins 16 bits, along
a au moins 32 bits, along long
a au moins 64 bits. Afloat
peut au moins représenter correctement les 6 chiffres décimaux les plus significatifs. Adouble
peut au moins représenter correctement les 10 chiffres décimaux les plus significatifs.Certes, sur la plupart des machines, nous aurons un complément à deux et des flotteurs IEEE 754.
la source
int mult(int a,int b) { return (long)a*b;}
[par exemple, siint
est 32 bits, mais registres etlong
sont 64]. Sans une telle exigence, le comportement "naturel" de l'implémentation la plus rapide delong l=mult(1000000,1000000);
seraitl
égal à1000000000000
, même si c'est une valeur "impossible" pour unint
.Celui-ci, ça va:
Aucun pointeur de données ne peut jamais être identique à un pointeur de fonction valide.
Ceci est VRAI pour tous les modèles plats, MS-DOS TINY, LARGE et HUGE, faux pour le modèle MS-DOS SMALL, et presque toujours faux pour les modèles MEDIUM et COMPACT (dépend de l'adresse de chargement, vous aurez besoin d'un très ancien DOS pour le rendre vrai).
Je ne peux pas écrire de test pour ça
Et pire: les pointeurs lancés vers ptrdiff_t peuvent être comparés. Ce n'est pas vrai pour le modèle MS-DOS LARGE (la seule différence entre LARGE et HUGE est HUGE ajoute le code du compilateur pour normaliser les pointeurs).
Je ne peux pas écrire de test car l'environnement dans lequel ces bombes durent n'allouera pas un tampon supérieur à 64K, donc le code qui le démontre planterait sur d'autres plates-formes.
Ce test particulier passerait sur un système maintenant disparu (notez que cela dépend des composants internes de malloc):
la source
EDIT: mis à jour vers la dernière version du programme
Solaris-SPARC
gcc 3.4.6 en 32 bits
gcc 3.4.6 en 64 bits
et avec SUNStudio 11 32 bits
et avec SUNStudio 11 64 bits
la source
Vous pouvez utiliser text-mode (
fopen("filename", "r")
) pour lire n'importe quel type de fichier texte.Bien que cela devrait en théorie fonctionner très bien, si vous utilisez également
ftell()
dans votre code et que votre fichier texte a des fins de ligne de style UNIX, dans certaines versions de la bibliothèque standard Windows,ftell()
des valeurs non valides seront souvent renvoyées. La solution consiste à utiliser le mode binaire à la place (fopen("filename", "rb")
).la source
gcc 3.3.2 sur AIX 5.3 (oui, nous devons mettre à jour gcc)
la source
Une hypothèse que certains peuvent faire en C ++ est que a
struct
est limité à ce qu'il peut faire en C. Le fait est que, en C ++, astruct
est comme aclass
sauf qu'il a tout public par défaut.Structure C ++:
la source
Les fonctions mathématiques standard sur différents systèmes ne donnent pas des résultats identiques.
la source
Visual Studio Express 2010 sur x86 32 bits.
la source
Via Codepad.org (
C++: g++ 4.1.2 flags: -O -std=c++98 -pedantic-errors -Wfatal-errors -Werror -Wall -Wextra -Wno-missing-field-initializers -Wwrite-strings -Wno-deprecated -Wno-unused -Wno-non-virtual-dtor -Wno-variadic-macros -fmessage-length=0 -ftemplate-depth-128 -fno-merge-constants -fno-nonansi-builtins -fno-gnu-keywords -fno-elide-constructors -fstrict-aliasing -fstack-protector-all -Winvalid-pch
).Notez que Codepad n'avait pas
stddef.h
. J'ai supprimé le test 9 en raison du codepad utilisant des avertissements comme erreurs. J'ai également renommé lacount
variable car elle était déjà définie pour une raison quelconque.la source
Qu'en est-il du décalage vers la droite par des quantités excessives - est-ce que cela est autorisé par la norme ou vaut-il la peine d'être testé?
La norme C spécifie-t-elle le comportement du programme suivant:
Sur au moins un compilateur que j'utilise, ce code échouera à moins que l'argument de print_string ne soit un "char const *". La norme permet-elle une telle restriction?
Certains systèmes permettent de produire des pointeurs vers des 'int' non alignés et d'autres non. Cela pourrait valoir la peine d'être testé.
la source
<<
et>>
). C99 a un langage identique au §6.5.7-3.putch
(pourquoi n'avez-vous pas utilisé le standardputchar
?), Je ne vois aucun comportement indéfini dans votre programme. C89 §3.1.4 spécifie qu '«un littéral de chaîne de caractères a […] type' tableau de char '» (note: nonconst
), et que «si le programme tente de modifier un littéral de chaîne […], le comportement est indéfini» . De quel compilateur s'agit-il et comment traduit-il ce programme?Pour info, pour ceux qui doivent traduire leurs compétences C en Java, voici quelques pièges.
En Java, char est 16 bits et signé. l'octet est de 8 bits et signé.
long est toujours 64 bits, les références peuvent être 32 bits ou 64 bits (si vous avez plus d'une application avec plus de 32 Go) Les JVM 64 bits utilisent généralement des références 32 bits.
Le décalage est masqué de sorte que i << 64 == i == i << -64, i << 63 == i << -1
ByteOrder.nativeOrder () peut être BIG_ENDIAN ou LITTLE_ENDIAN
i = i++
ne change jamaisi
La taille des collections et des tableaux est toujours de 32 bits, que la JVM soit 32 bits ou 64 bits.
char est 16 bits, court est 16 bits, int est 32 bits et long est 64 bits.
la source