La règle ssortingcte sur le crénelage est-elle vraiment une «voie à double sens»?

Dans ces commentaires, l’ utilisateur @Deduplicator insiste sur le fait que la règle d’aliasing ssortingcte autorise l’access via un type incompatible si le pointeur aliasé ou aliasing est un type pointeur-caractère (qualifié ou non qualifié, signé ou non signé). Donc, son affirmation est fondamentalement que les deux

 long long foo; char *p = (char *)&foo; *p; // just in order to dereference 'p' 

et

 char foo[sizeof(long long)]; long long *p = (long long *)&foo[0]; *p; // just in order to dereference 'p' 

sont conformes et ont un comportement défini.

Dans ma lecture, toutefois, seule la première forme est valide, c’est-à-dire lorsque le pointeur aliasing est un pointeur sur un caractère; Toutefois, cela ne peut pas être fait dans l’autre sens, c’est-à-dire lorsque le pointeur d’aliasing pointe vers un type incompatible (autre qu’un type de caractère), le pointeur aliasé étant un caractère char * .

Ainsi, le deuxième extrait ci-dessus aurait un comportement indéfini.

Quel est le cas Est-ce correct? Pour mémoire, j’ai déjà lu cette question et cette réponse , et la réponse acceptée y indique explicitement que

Les règles autorisent une exception pour char * . Il est toujours supposé que char * alias d’autres types. Cependant, cela ne fonctionnera pas dans l’autre sens, il n’y a aucune hypothèse que votre struct alias un tampon de caractères.

(c’est moi qui souligne)

Vous avez raison de dire que ce n’est pas valide. Comme vous l’avez vous-même cité (je ne reviendrai donc pas ici), la dissortingbution valide garantie ne provient que de tout autre type à char *.

L’autre forme est en effet contre standard et provoque un comportement indéfini. Cependant, comme petit bonus, discutons un peu en arrière de cette norme.

Les caractères, sur chaque architecture significative, sont le seul type qui autorise un access totalement non aligné, ceci est dû au fait que les instructions de lecture d’octet doivent fonctionner sur n’importe quel octet, sinon elles seraient pratiquement inutiles. Cela signifie qu’une lecture indirecte sur un caractère sera toujours valable sur tous les processeurs que je connaisse.

Cependant, l’inverse ne s’applique pas, vous ne pouvez pas lire un uint64_t à moins que le pointeur ne soit aligné sur 8 octets sur la plupart des arches.

Cependant, il existe une extension très courante du compilateur qui vous permet de convertir des pointeurs correctement alignés d’un caractère à un autre et d’y accéder, mais qu’il s’agit d’une méthode non standard. Notez également que si vous convertissez un pointeur en un type quelconque en un caractère puis en le rediffusant, le pointeur résultant est garanti égal à l’object d’origine. Donc c’est ok:

 struct x *mystruct = MakeAMyStruct(); char * foo = (char *)mystruct; struct x *mystruct2 = (struct mystruct *)foo; 

Et mystruct2 sera égal à mystruct. Cela garantit également que la structure est correctement alignée pour ses besoins.

Donc, fondamentalement, si vous voulez un pointeur sur un caractère et un pointeur sur un autre type, déclarez toujours le pointeur sur l’autre type, puis transformez en caractère. Ou encore mieux, utilisez un syndicat, c’est ce à quoi ils sont fondamentalement destinés …

Notez qu’il existe une exception notable à la règle cependant. Certaines anciennes implémentations de malloc retournaient un char *. Il est toujours garanti que ce pointeur puisse être converti en n’importe quel type avec succès sans enfreindre les règles d’alias.

Le déduplicateur est correct. Le comportement non défini qui permet aux compilateurs d’implémenter des optimisations de “crénelage ssortingct” ne s’applique pas lorsque des valeurs de caractère sont utilisées pour produire une représentation d’un object.

Certaines représentations d’object ne doivent pas nécessairement représenter une valeur du type d’object. Si la valeur stockée d’un object a une telle représentation et est lue par une expression lvalue qui n’a pas de type de caractère, le comportement est indéfini. Si une telle représentation est produite par un effet secondaire qui modifie tout ou partie de l’object par une expression lvalue qui n’a pas de type de caractère, le comportement est indéfini. Une telle représentation s’appelle une représentation de piège.

Cependant, votre deuxième exemple a un comportement indéfini car foo n’est pas initialisé. Si vous initialisez foo comportement défini par l’implémentation est défini. Cela dépend des exigences d’alignement définies par l’implémentation long long et si long long possède des bits de pad définis par l’implémentation.

Considérez si vous changez votre deuxième exemple en ceci:

 long long bar() { char *foo = malloc(sizeof(long long)); char c; for(c = 0; c < sizeof(long long); c++) foo[c] = c; long long *p = (long long *) p; return *p; } 

Maintenant, l'alignement n'est plus un problème et cet exemple dépend uniquement de la représentation définie par implémentation de long long . La valeur renvoyée dépend de la représentation de long long mais si cette représentation est définie comme n’ayant pas de bits de remplissage, cette fonction doit toujours renvoyer la même valeur, qui doit également être une valeur valide . Sans les bits de pad, cette fonction ne peut pas générer de représentation d'interruption et le compilateur ne peut donc effectuer aucune optimisation de type aliasing ssortingcte.

Vous devez chercher assez difficile à trouver une implémentation standard conforme à C qui a des bits de pad définis par l'implémentation dans n'importe lequel de ses types entiers. Je doute que vous en trouviez un qui implémente tout type d'optimisation de type aliasing ssortingct. En d'autres termes, les compilateurs n'utilisent pas le comportement indéfini provoqué par l'access à une représentation d'interruption pour autoriser les optimisations d'alias ssortingct, car aucun compilateur implémentant des optimisations d'alias ssortingct n'a défini de représentation d'interruption.

Notez également que si toutes les zéros avaient été initialisés (caractères '\0' ), alors cette fonction n'aurait aucun comportement défini ou non défini. Une représentation de tous les bits à zéro d'un type entier est garantie ne pas être une représentation d'interruption et garantie d'avoir la valeur 0.

Passons maintenant à un exemple ssortingctement conforme qui utilise des valeurs char pour créer une représentation valide garantie (éventuellement non nulle) d'une valeur long long :

 #include  #include  int main(int argc, char **argv) { int i; long long l; char *buf; if (argc < 2) { return 1; } buf = malloc(sizeof l); if (buf == NULL) { return 1; } l = strtoll(argv[1], NULL, 10); for (i = 0; i < sizeof l; i++) { buf[i] = ((char *) &l)[i]; } printf("%lld\n", *(long long *)buf); return 0; } 

Cet exemple n'a pas de comportement indéfini et ne dépend pas de l'alignement ou de la représentation de long long . C'est le type de code pour lequel l'exception de type de caractère sur l'access aux objects a été créée. Cela signifie notamment que le standard C vous permet d’implémenter votre propre fonction de memcpy dans le code C portable.