Donner un type efficace aux données compte-t-il comme un effet secondaire?

Supposons que j’ai un bloc de données allouées dynamicment:

void* allocate (size_t n) { void* foo = malloc(n); ... return foo; } 

Je souhaite utiliser les données pointées par foo comme type spécial, type_t . Mais je veux le faire plus tard, et pas pendant l’atsortingbution. Afin de donner un type efficace aux données allouées, je peux donc faire quelque chose comme:

 void* allocate (size_t n) { void* foo = malloc(n); (void) *(type_t*)foo; ... return foo } 

Conformément à C11 6.5 / 6, cet access à la valeur lvalue doit donner le type effectif type_t :

Pour tous les autres access à un object n’ayant pas de type déclaré, le type effectif de l’object est simplement le type de la valeur lvalue utilisée pour l’access.

Cependant, la ligne (void) *(type_t*)foo; ne contient aucun effet secondaire, le compilateur devrait donc être libre de l’optimiser, et je ne m’attendrais pas à ce qu’il génère un code machine réel.

Ma question est la suivante: les astuces comme celle-ci sont-elles sûres? Donner un type efficace aux données compte-t-il comme un effet secondaire? Ou bien, en optimisant le code, le compilateur optimisera-t-il également le choix du type effectif?

C’est-à-dire qu’avec l’astuce d’access lvalue ci-dessus, si j’appelle maintenant la fonction ci-dessus comme ceci:

 int* i = allocate(sizeof(int)); *i = something; 

Cela provoque-t-il la violation d’alias ssortingcte UB comme prévu ou le type effectif est-il maintenant int ?

La phrase de la norme que vous citez clairement énonce seulement quelque chose à propos de l’ access à l’object. Les seules modifications apscopes au type effectif de l’ object que décrit la norme sont les deux phrases précédentes, qui décrivent clairement que vous devez stocker dans l’object avec le type que vous souhaitez rendre effectif.

6.5 / 6

Si une valeur est stockée dans un object n’ayant pas de type déclaré via une lvalue dont le type n’est pas un type de caractère, le type de la lvalue devient le type effectif de l’object pour cet access et pour les access suivants ne modifiant pas le valeur stockée.

Rien dans la norme ne suggérerait qu’une opération qui écrit dans un object devrait seulement être reconnue comme définissant le type effectif dans les cas où l’opération a également d’autres effets secondaires (comme changer le modèle de bits stockés dans cet object). . D’autre part, les compilateurs qui utilisent une optimisation agressive basée sur des types semblent incapables de reconnaître un changement possible du type effectif d’un object en tant qu’effet secondaire qui doit être conservé même si l’écriture n’aurait aucun autre effet secondaire observable.

Pour comprendre ce que dit réellement la règle Effective Type, il est nécessaire de comprendre d’où elle vient. Autant que je sache, cela semble provenir du rapport de défauts n ° 028, plus précisément de la justification utilisée pour justifier la conclusion qui y est donnée. La conclusion donnée est raisonnable, mais la justification donnée est absurde.

Le principe de base implique la possibilité de quelque chose comme:

 void actOnTwoThings(T1 *p1, T2 *p2) { ... code that uses p1 and p2 } ... ...in some other function union {T1 v1; T2 v2; } u; actOnTwoThings(&u.v1, &u.v2); 

Parce que le fait d’écrire une union comme un type et de lire comme un autre donne un comportement défini par l’implémentation, le comportement d’écrire un membre de l’union via un pointeur et d’en lire un autre n’est pas entièrement défini par la norme, et doit donc (selon la logique de DR # 028) être traité comme un comportement indéfini. Bien que l’utilisation de p1 et de p2 pour accéder au même stockage doive en fait être traitée comme un UB dans de nombreux scénarios similaires à ceux décrits ci-dessus, la logique est totalement erronée. Spécifier qu’une action génère un comportement défini par l’implémentation est très différent de dire qu’elle génère un comportement indéfini, en particulier dans les cas où la norme imposerait des limites au comportement que pourrait définir l’implémentation.

Un résultat essentiel des règles de type pointeur dérivées du comportement des unions est que le comportement est défini de manière complète et sans ambiguïté, sans aucun aspect défini par Implémentation, si le code écrit une union autant de fois que nécessaire, dans un ordre quelconque, puis lit le dernier membre écrit. Bien que le fait d’imposer que les implémentations le permettent, bloque certaines optimisations utiles, il est clair que les règles de type Effective sont écrites pour imposer un tel comportement.

Un problème plus important qui découle du fait de fonder les règles de types sur le comportement des syndicats est que l’action de lire un syndicat en utilisant un type et d’écrire l’union avec un autre type ne doit pas être considérée comme ayant des effets secondaires si le nouveau modèle de bits correspond au modèle précédent. . Dans la mesure où une implémentation devrait définir le nouveau modèle de bits comme représentant la valeur écrite en tant que nouveau type, elle devrait également définir l’ancien modèle de bits (identique) comme représentant la même valeur. Étant donné la fonction (supposons que «long» et «long long» sont du même type):

  long test(long *p1, long long *p2, void *p3) { if (*p1) { long long temp; *p2 = 1; temp = *(long long*)p3; *(long*)p3 = temp; } return *p1; } 

gcc et clang décideront que l’écriture via *(long*)p3 ne peut avoir aucun effet, car elle enregistre simplement le même motif binary lu par *(long long*)p3 , ce qui serait le cas si le les lectures suivantes de *p1 devaient être traitées dans le comportement Implementation-Defined dans le cas où le stockage était écrit via *p2 , mais n’est pas vrai si ce cas est considéré comme UB. Malheureusement, dans la mesure où la norme n’indique pas si le comportement est défini ou non défini par l’implémentation, il est incohérent de savoir si l’écriture doit être considérée comme un effet secondaire.

D’un sharepoint vue pratique, lorsque vous n’utilisez pas -fno-ssortingct-aliasing , gcc et clang doivent être considérés comme le traitement d’un dialecte de C où les types effectifs, une fois définis, deviennent permanents. Ils ne peuvent pas reconnaître de manière fiable tous les cas dans lesquels les types effectifs peuvent être changés, et la logique nécessaire pour traiter facilement et efficacement de nombreux cas que les auteurs de gcc prétendent depuis longtemps ne peuvent éventuellement pas être traités sans optimisation.