Est-ce que la conversion entre un comportement indéfini pointeur-T, tableau-T et pointeur-tableau-T

Considérons le code suivant.

#include  int main() { typedef int T; T a[] = { 1, 2, 3, 4, 5, 6 }; T(*pa1)[6] = (T(*)[6])a; T(*pa2)[3][2] = (T(*)[3][2])a; T(*pa3)[1][2][3] = (T(*)[1][2][3])a; T *p = a; T *p1 = *pa1; //T *p2 = *pa2; //error in c++ //T *p3 = *pa3; //error in c++ T *p2 = **pa2; T *p3 = ***pa3; printf("%p %p %p %p %p %p %p\n", a, pa1, pa2, pa3, p, p1, p2, p3); printf("%d %d %d %d %d %d %d\n", a[5], (*pa1)[5], (*pa2)[2][1], (*pa3)[0][1][2], p[5], p1[5], p2[5], p3[5]); return 0; } 

Le code ci-dessus est compilé et exécuté en C, produisant les résultats attendus. Toutes les valeurs du pointeur sont les mêmes, de même que toutes les valeurs int. Je pense que le résultat sera le même pour tous les types T, mais int est le plus facile à utiliser.

Au début, j’ai avoué que j’étais étonné que le fait de déréférencer un pointeur sur un tableau donne une valeur de pointeur identique, mais si je réfléchis bien, je pense que c’est simplement l’inverse de la désintégration tableau à pointeur que nous connaissons et aimons.

[EDIT: Les lignes commentées déclenchent des erreurs en C ++ et des avertissements en C. Je trouve la norme C vague sur ce point, mais ce n’est pas la vraie question.]

Dans cette question, il a été prétendu être un comportement indéfini, mais je ne le vois pas. Ai-je raison?

Code ici si tu veux le voir.


Juste après avoir écrit ce qui précède, je me suis rendu compte que ces erreurs sont dues au fait qu’il n’ya qu’un seul niveau de désintégration du pointeur en C ++. Plus de déréférencement est nécessaire!

  T *p2 = **pa2; //no error in c or c++ T *p3 = ***pa3; //no error in c or c++ 

Et avant d’avoir réussi à terminer cette modification, @AntonSavin a fourni la même réponse. J’ai édité le code pour refléter ces changements.

Ceci est une réponse en C uniquement.

C11 (n1570) 6.3.2.3 p7

Un pointeur sur un type d’object peut être converti en un pointeur sur un type d’object différent. Si le pointeur résultant n’est pas correctement aligné *) pour le type référencé, le comportement est indéfini. Sinon, lorsqu’il sera reconverti, le résultat sera comparable au pointeur original.

*) En général, le concept «correctement aligné» est transitif: si un pointeur sur le type A est correctement aligné pour un pointeur sur le type B , lui-même correctement aligné sur un pointeur sur le type C , alors un pointeur sur le type A est correctement aligné pour un pointeur sur C

La norme est un peu vague sur ce qui se passe si nous utilisons un tel pointeur (sauf le repli sur le pseudonyme ssortingct) pour autre chose que la reconvertir, mais l’intention et l’interprétation répandue est que ces pointeurs doivent se comparer et avoir la même valeur numérique. Par exemple, ils devraient également être égaux lors de la conversion en uintptr_t ). Par exemple, pensez à (void *)array == (void *)&array (la conversion en char * au lieu de void * est explicitement garantie de fonctionner).

 T(*pa1)[6] = (T(*)[6])a; 

C’est bien, le pointeur est correctement aligné (c’est le même pointeur que &a ).

 T(*pa2)[3][2] = (T(*)[3][2])a; // (i) T(*pa3)[1][2][3] = (T(*)[1][2][3])a; // (ii) 

Iff T[6] a les mêmes exigences d’alignement que T[3][2] , et les mêmes que T[1][2][3] , (i) et (ii) sont sûrs, respectivement. Pour moi, cela semble étrange, ils ne pourraient pas, mais je ne peux pas trouver une garantie dans la norme qu’ils devraient avoir les mêmes exigences d’alignement.

 T *p = a; // safe, of course T *p1 = *pa1; // *pa1 has type T[6], after lvalue conversion it's T*, OK T *p2 = **pa2; // **pa2 has type T[2], or T* after conversion, OK T *p3 = ***pa3; // ***pa3, has type T[3], T* after conversion, OK 

En ignorant l’UB causé par la transmission de int *printf attend void * , examinons les expressions contenues dans les arguments de la prochaine printf , en premier lieu celles définies:

 a[5] // OK, of course (*pa1)[5] (*pa2)[2][1] (*pa3)[0][1][2] p[5] // same as a[5] p1[5] 

Notez que l’aliasing ssortingct n’est pas un problème ici, aucune lvalue mal typée n’est impliquée, et nous accédons à T tant que T

Les expressions suivantes dépendent de l’interprétation de l’arithmétique de pointeur hors limites, l’interprétation la plus détendue (autoriser container_of , aplatissement de tableau , le «struct hack» avec char[] , etc.) les autorise également; l’interprétation plus ssortingcte (autoriser une implémentation fiable de vérification des limites à l’exécution pour l’arithmétique et le déréférencement de pointeur, mais interdire le container_of , l’aplatissement de tableau (mais pas nécessairement le «levage» de tableau, ce que vous avez fait), la struct hack, etc. :

 p2[5] // UB, p2 points to the first element of a T[2] array p3[5] // UB, p3 points to the first element of a T[3] array 

La seule raison pour laquelle votre code est compilé en C est que la configuration par défaut du compilateur autorise le compilateur à effectuer implicitement certaines conversions de pointeur illégales. Formellement, cela n’est pas autorisé par le langage C. Ces lignes

 T *p2 = *pa2; T *p3 = *pa3; 

sont mal formés en C ++ et produisent des violations de contrainte en C. En langage simple, ces lignes sont des erreurs à la fois en langage C et C ++.

Tout compilateur C qui se respecte se verra (est en fait obligé d’émettre) des messages de diagnostic pour ces violations de contrainte. Le compilateur GCC, par exemple, émettra des “avertissements” vous indiquant que les types de pointeurs dans les initialisations ci-dessus sont incompatibles. Bien que les “avertissements” suffisent parfaitement pour satisfaire aux exigences standard, si vous voulez vraiment utiliser la capacité du compilateur GCC à reconnaître les contraintes violant le code C, vous devez l’exécuter avec le -pedantic-errors et, de préférence, explicitement sélectionner la version de langage standard à l’aide de -std= commutateur.

Dans votre expérience, le compilateur C a effectué ces conversions implicites pour vous en tant qu’extension de compilateur non standard. Cependant, le fait que le compilateur GCC exécuté sous ideone front ait complètement supprimé les messages d’avertissement correspondants (émis par le compilateur autonome GCC même dans sa configuration par défaut) signifie qu’idéone est un compilateur C cassé. On ne peut pas vraiment compter sur sa sortie de diagnostic pour distinguer un code C valide d’un code invalide.

Quant à la conversion elle-même … Effectuer cette conversion n’est pas un comportement indéfini. Cependant, le fait d’accéder aux données d’un tableau via les pointeurs convertis constitue un comportement indéfini.

UPDATE: Ce qui suit s’applique uniquement à C ++ . Pour C, faites défiler vers le bas. En bref, il n’y a pas d’UB en C ++ et il y a UB en C.

8.3.4/7 dit:

Une règle cohérente est suivie pour les tableaux multidimensionnels. Si E est un tableau à n dimensions du rang ixjx … xk, alors E apparaissant dans une expression soumise à la conversion tableau à pointeur (4.2) est converti en un pointeur vers une dimension (n – 1) tableau de rang jx … x k. Si l’ opérateur * , explicitement ou implicitement à la suite de l’index, est appliqué à ce pointeur, le résultat est le tableau à dimensions (n ​​- 1) pointé, qui est immédiatement converti en pointeur.

Donc, cela ne produira pas d’erreur en C ++ (et fonctionnera comme prévu):

 T *p2 = **pa2; T *p3 = ***pa3; 

Que ce soit UB ou non. Considérez la toute première conversion:

 T(*pa1)[6] = (T(*)[6])a; 

En C ++ c’est en fait

 T(*pa1)[6] = reinterpret_cast(a); 

Et voici ce que dit le standard à propos de reinterpret_cast :

Un pointeur d’object peut être explicitement converti en un pointeur d’object d’un type différent. Lorsqu’une prvalue v de type “pointeur sur T1” est convertie en type “pointeur vers cv T2”, le résultat est static_cast * > (static_cast * > (v)) si T1 et T2 sont standard. Les types de calque (3.9) et les exigences d’alignement de T2 ne sont pas plus ssortingctes que celles de T1 ou si l’un de ces types est nul.

Donc, a est converti en pa1 via static_cast en void* et en retour. La conversion statique en void* garantit le renvoi de l’adresse réelle d’ a comme indiqué en 4.10/2 :

Une valeur de type “pointeur sur cv T”, où T est un type d’object, peut être convertie en une valeur de type “pointeur sur cv void”. Le résultat de la conversion d’une valeur de pointeur non nulle d’un pointeur en type d’object en un «pointeur sur cv void» représente l’adresse du même octet en mémoire que la valeur de pointeur d’origine.

La conversion statique suivante en T(*)[6] garantit à nouveau le retour de la même adresse, comme indiqué en 5.2.9/13 :

Une valeur de type “pointeur sur cv1 vide” peut être convertie en une valeur de type “pointeur sur cv2 T”, où T est un type d’object et cv2 est identique à cv-qualification ou supérieur à cv-qualification. La valeur du pointeur null est convertie en la valeur du pointeur null du type de destination. Si la valeur du pointeur d’origine représente l’adresse A d’un octet en mémoire et que A satisfait à l’exigence d’alignement de T, la valeur du pointeur résultante représente la même adresse que la valeur du pointeur d’origine, c’est-à-dire A

Ainsi, le pa1 garantit de pointer vers le même octet en mémoire qu’en tant a , de sorte que l’access aux données par ce biais est parfaitement valide car l’alignement des tableaux est identique à celui du type sous-jacent.

Qu’en est-il de C?

Considérer à nouveau:

 T(*pa1)[6] = (T(*)[6])a; 

Dans la norme C11, 6.3.2.3/7 indique ce qui suit:

Un pointeur sur un type d’object peut être converti en un pointeur sur un type d’object différent. Si le pointeur résultant n’est pas correctement aligné pour le type référencé, le comportement est indéfini. Sinon, lorsqu’il sera reconverti, le résultat sera comparable au pointeur original. Lorsqu’un pointeur sur un object est converti en un pointeur sur un type de caractère, le résultat pointe sur l’octet adressé le plus bas de l’object. Des incréments successifs du résultat, jusqu’à la taille de l’object, donnent des pointeurs sur les octets restants de l’object.

Cela signifie que, sauf si la conversion est au char* , la valeur du pointeur converti n’est pas garantie égale à la valeur du pointeur d’origine , ce qui entraîne un comportement non défini lors de l’access aux données via le pointeur converti. Pour que cela fonctionne, la conversion doit s’effectuer explicitement à l’aide de void* :

 T(*pa1)[6] = (T(*)[6])(void*)a; 

Reconversion en T *

 T *p = a; T *p1 = *pa1; T *p2 = **pa2; T *p3 = ***pa3; 

Ce sont toutes des conversions de array of T en pointer to T en pointer to T , qui sont valables à la fois en C ++ et en C, et aucun UB n’est déclenché par l’access aux données par le biais de pointeurs convertis.