web-dev-qa-db-fra.com

Dans une structure, est-il légal d'utiliser un champ de tableau pour en accéder un autre?

A titre d'exemple, considérons la structure suivante:

struct S {
  int a[4];
  int b[4];
} s;

Serait-il légal d'écrire s.a[6] et de m'attendre à ce qu'il soit égal à s.b[2]? Personnellement, je pense que ce doit être UB en C++, alors que je ne suis pas sûr de C . pertinent dans les standards des langages C et C++.


Mettre à jour

Plusieurs réponses suggèrent des moyens de s’assurer qu’il n’ya pas de remplissage entre les champs afin que le code fonctionne de manière fiable. Je voudrais souligner Que si un tel code est UB, alors l’absence de remplissage ne suffit pas. Si c'est UB, Le compilateur est alors libre de supposer que les accès à S.a[i] et S.b[j] ne se chevauchent pas et que le compilateur est libre de réorganiser ces accès à la mémoire. Par exemple,

    int x = s.b[2];
    s.a[6] = 2;
    return x;

peut être transformé en

    s.a[6] = 2;
    int x = s.b[2];
    return x;

qui retourne toujours 2.

51
Nikolai

Serait-il légal d'écrire s.a [6] et de s'attendre à ce qu'il soit égal à s.b [2]?

Non. Parce que l'accès à un tableau hors limites appelle comportement indéfini en C et C++.

C11 J.2 Comportement non défini

  • L'addition ou la soustraction d'un pointeur dans ou juste au-delà d'un objet de tableau et d'un type entier produit un résultat qui pointe juste au-delà de l'objet tableau et est utilisé comme opérande d'un opérateur unaire * qui est évalué (6.5.6).

  • Un indice de tableau est hors limites, même si un objet est apparemment accessible avec l'indice donné (comme dans l'expression lvalue a[1][7] étant donné la déclaration int a[4][5]) (6.5.6).

C++ standard draft section 5.7 Opérateurs d'additifs Le paragraphe 5 dit:

Lorsqu'une expression de type intégral est ajoutée ou soustraite à partir d'un pointeur, le résultat a le type de l'opérande du pointeur. Si la pointeur opérande pointe vers un élément d'un objet tableau et le tableau est assez grand, le résultat pointe vers un élément décalé par rapport à élément d'origine tel que la différence entre les indices du les éléments de tableau résultants et originaux sont égaux à l'expression intégrale . [...] Si l'opérande de pointeur et le résultat pointent sur des éléments du même objet de tableau, ou un après le dernier élément du tableau objet, l'évaluation ne doit pas produire de débordement; sinon, le le comportement est indéfini.

61
M.S Chaudhari

Mis à part la réponse de @rsp (Undefined behavior for an array subscript that is out of range), je peux ajouter qu'il n'est pas légal d'accéder à b via a car le langage C ne spécifie pas la quantité d'espace de remplissage pouvant être comprise entre la fin de la zone allouée pour a et le début de b, même si vous pouvez l'exécuter sur une implémentation particulière, ce n'est pas portable.

instance of struct:
+-----------+----------------+-----------+---------------+
|  array a  |  maybe padding |  array b  | maybe padding |
+-----------+----------------+-----------+---------------+

Le deuxième remplissage peut manquer ainsi que l'alignement de struct object est l'alignement de a qui est identique à l'alignement de b, mais le langage C n'impose pas non plus que le second remplissage soit absent.

34
alinsoar

a et b sont deux tableaux différents, et a est défini comme contenant des éléments 4. Par conséquent, a[6] accède au tableau en dehors des limites et constitue donc un comportement indéfini. Notez que l’indice de tableau a[6] est défini par *(a+6), de sorte que la preuve de UB est en fait donnée par la section "Opérateurs additifs" en conjonction avec des pointeurs ". Consultez la section suivante de la norme C11 (par exemple, this version préliminaire en ligne). décrivant cet aspect:

6.5.6 Opérateurs additifs

Lorsqu'une expression de type entier est ajoutée ou soustraite à partir d'un pointeur, le résultat a le type de l'opérande du pointeur. Si la pointeur opérande pointe vers un élément d'un objet tableau et le tableau est assez grand, le résultat pointe vers un élément décalé par rapport à élément d'origine tel que la différence entre les indices du les éléments de tableau résultants et originaux sont égaux à l'expression entière . En d'autres termes, si l'expression P pointe sur le i-ème élément d'un objet tableau, les expressions (P) + N (de manière équivalente, N + (P)) et (P) -N (où N a la valeur n) pointe sur, respectivement, le i + n-ème et i-n-ième éléments de l'objet tableau, à condition qu'ils existent. De plus, si l'expression P pointe vers le dernier élément d'un objet tableau, le expression (P) +1 pointe un après le dernier élément de l'objet tableau, et si l'expression Q pointe un après le dernier élément d'un tableau objet, l'expression (Q) -1 pointe sur le dernier élément du tableau objet. Si l'opérande de pointeur et le résultat pointent sur des éléments du même objet de tableau, ou un après le dernier élément du tableau objet, l'évaluation ne doit pas produire de débordement; sinon, le le comportement est indéfini. Si le résultat pointe un après le dernier élément de l'objet tableau, il ne doit pas être utilisé comme opérande d'un unaire * opérateur qui est évalué.

Le même argument s'applique au C++ (bien que non cité ici).

De plus, bien qu'il s'agisse clairement d'un comportement indéfini dû au fait de dépasser les limites du tableau de a, notez que le compilateur peut introduire un remplissage entre les membres a et b, de sorte que, même si un arithmétique à pointeur était autorisé, a+6 ne produirait pas nécessairement le même adresse comme b+2.

11
Stephan Lechner

Est-ce légal? Comme d’autres l'ont mentionné, il invoque Un comportement non défini .

Est-ce que ça marchera? Cela dépend de votre compilateur. C'est le problème du comportement indéfini: c'est indéfini

Sur beaucoup de compilateurs C et C++, la structure sera disposée de telle sorte que b suivra immédiatement un en mémoire et qu’il n’y aura pas de vérification des limites. Donc, accéder à un [6] sera effectivement le même que b [2] et ne causera aucune exception. 

Donné

struct S {
  int a[4];
  int b[4];
} s

et en supposant qu'il n'y ait pas de bourrage supplémentaire , la structure n'est en fait qu'un moyen de voir un bloc de mémoire contenant 8 entiers. Vous pouvez le convertir en (int*) et ((int*)s)[6] pointera vers la même mémoire que s.b[2]

Devriez-vous compter sur ce genre de comportement? Absolument pas. Undefined signifie que le compilateur n'a pas à supporter cela. Le compilateur est libre de remplir la structure, ce qui pourrait rendre l’hypothèse incorrecte sur & (s.b [2]) == & (s.a [6]). Le compilateur pourrait également ajouter des limites pour vérifier l'accès au tableau (bien que l'activation des optimisations du compilateur désactiverait probablement une telle vérification).

J'ai déjà expérimenté les effets de ceci. Il est assez courant d'avoir une structure comme celle-ci

struct Bob {
    char name[16];
    char whatever[64];
} bob;
strcpy(bob.name, "some name longer than 16 characters");

Maintenant, bob.whats seront "que 16 caractères". (c'est pourquoi vous devriez toujours utiliser strncpy, BTW)

6
dwilliss

Comme @MartinJames l'a mentionné dans un commentaire, si vous devez vous assurer que a et b sont en mémoire contiguë (ou au moins pouvant être traitées comme telles, (édition) à moins que votre architecture/compilateur utilise une taille de bloc mémoire inhabituelle/offset et forcée. l’alignement nécessitant l’ajout de rembourrage), vous devez utiliser une variable union.

union overlap {
    char all[8]; /* all the bytes in sequence */
    struct { /* (anonymous struct so its members can be accessed directly) */
        char a[4]; /* padding may be added after this if the alignment is not a sub-factor of 4 */
        char b[4];
    };
};

Vous ne pouvez pas accéder directement à b à partir de a (par exemple a[6], comme vous l'avez demandé), mais vous pouvez accéder aux éléments de a et b à l'aide de all (par exemple, all[6] fait référence au même emplacement mémoire que b[2]).

(Édition: vous pouvez remplacer 8 et 4 dans le code ci-dessus par 2*sizeof(int) et sizeof(int), respectivement, pour être plus susceptible de correspondre à l'alignement de l'architecture, en particulier si le code doit être plus portable, mais vous devez faire attention à ne pas créer toute hypothèse sur le nombre d'octets dans a, b ou all. Toutefois, cela fonctionnera sur les alignements de mémoire probablement les plus courants (1, 2 et 4 octets).)

Voici un exemple simple:

#include <stdio.h>

union overlap {
    char all[2*sizeof(int)]; /* all the bytes in sequence */
    struct { /* anonymous struct so its members can be accessed directly */
        char a[sizeof(int)]; /* low Word */
        char b[sizeof(int)]; /* high Word */
    };
};

int main()
{
    union overlap testing;
    testing.a[0] = 'a';
    testing.a[1] = 'b';
    testing.a[2] = 'c';
    testing.a[3] = '\0'; /* null terminator */
    testing.b[0] = 'e';
    testing.b[1] = 'f';
    testing.b[2] = 'g';
    testing.b[3] = '\0'; /* null terminator */
    printf("a=%s\n",testing.a); /* output: a=abc */
    printf("b=%s\n",testing.b); /* output: b=efg */
    printf("all=%s\n",testing.all); /* output: all=abc */

    testing.a[3] = 'd'; /* makes printf keep reading past the end of a */
    printf("a=%s\n",testing.a); /* output: a=abcdefg */
    printf("b=%s\n",testing.b); /* output: b=efg */
    printf("all=%s\n",testing.all); /* output: all=abcdefg */

    return 0;
}
5
Jed Schaaf

Non , car l'accès à un tableau en dehors des limites appelle Undefined Behavior, à la fois en C et en C++.

3
gsamaras

Réponse courte: Non. Vous êtes au pays d'un comportement indéfini.

Réponse longue: Non. Mais cela ne veut pas dire que vous ne pouvez pas accéder aux données autrement: si vous utilisez GCC, vous pouvez procéder de la manière suivante (élaboration de la réponse de dwillis):

struct __attribute__((packed,aligned(4))) Bad_Access {
    int arr1[3];
    int arr2[3];
};

et ensuite vous pourriez accéder via ( source Godbolt + asm ):

int x = ((int*)ba_pointer)[4];

Mais cette conversion viole l'aliasing strict et n'est donc sécurisée qu'avec g++ -fno-strict-aliasing. Vous pouvez transtyper un pointeur struct sur un pointeur vers le premier membre, mais vous revenez dans le bateau UB car vous accédez en dehors du premier membre.

Sinon, ne faites pas ça. Sauvez un futur programmeur (probablement vous-même) du chagrin d'amour de ce gâchis.

Aussi, pendant que nous y sommes, pourquoi ne pas utiliser std :: vector? Ce n'est pas infaillible, mais à l'arrière il y a des gardes pour empêcher un tel mauvais comportement.

Addenda:

Si vous êtes vraiment préoccupé par la performance:

Disons que vous avez accès à deux pointeurs du même type. Le compilateur supposera plus que probablement que les deux pointeurs ont la possibilité d'interférer et instanciera une logique supplémentaire pour vous protéger contre toute action stupide.

Si vous jurez solennellement au compilateur que vous n'essayez pas d'alias, le compilateur vous récompensera généreusement: Le mot-clé restrict offre-t-il des avantages significatifs dans gcc/g ++

Conclusion: ne sois pas méchant; votre futur moi, et le compilateur vous en remercieront.

1
Alex Shirley

La réponse de Jed Schaff est sur la bonne voie, mais pas tout à fait correcte. Si le compilateur insère une marge entre a et b, sa solution échouera quand même. Si, toutefois, vous déclarez:

typedef struct {
  int a[4];
  int b[4];
} s_t;

typedef union {
  char bytes[sizeof(s_t)];
  s_t s;
} u_t;

Vous pouvez maintenant accéder à (int*)(bytes + offsetof(s_t, b)) pour obtenir l'adresse de s.b, quelle que soit la manière dont le compilateur affiche la structure. La macro offsetof() est déclarée dans <stddef.h>.

L'expression sizeof(s_t) est une expression constante, légale dans une déclaration de tableau à la fois en C et en C++. Cela ne donnera pas un tableau de longueur variable. (Toutes mes excuses pour avoir mal interprété le standard C auparavant. Je pensais que cela semblait faux.)

Dans le monde réel, cependant, deux tableaux consécutifs de int dans une structure vont être disposés comme vous le souhaitez. (Vous pourriez pouvoir créer un contre-exemple très complexe en définissant la limite de a à 3 ou 5 au lieu de 4, puis en demandant au compilateur d'aligner à la fois a et b sur une limite de 16 octets.) méthodes pour essayer d’obtenir un programme qui n’émet aucune hypothèse au-delà du libellé strict de la norme, vous voulez une sorte de codage défensif, tel que static assert(&both_arrays[4] == &s.b[0], "");. Celles-ci n’ajoutent aucune surcharge d’exécution et échoueront si votre compilateur fait quelque chose qui briserait votre programme, tant que vous ne déclencherez pas UB dans l’affirmation elle-même.

Si vous voulez un moyen portable de garantir que les deux sous-tableaux sont regroupés dans une plage de mémoire contiguë ou divisez un bloc de mémoire dans l'autre sens, vous pouvez les copier avec memcpy().

1
Davislor

La norme n'impose aucune restriction quant à la mise en œuvre à effectuer lorsqu'un programme tente d'utiliser un indice de tableau hors limites dans un champ de structure pour accéder à un membre d'un autre. Les accès hors limites sont donc "illégaux" dans des programmes strictement conformes , et les programmes qui utilisent ces accès ne peuvent pas être simultanément 100% portables et exempts d'erreurs. Par ailleurs, de nombreuses implémentations définissent le comportement de ce code et les programmes qui ne visent que ces implémentations peuvent exploiter ce comportement.

Un tel code pose trois problèmes:

  1. Alors que de nombreuses implémentations présentent les structures de manière prévisible, la norme permet aux implémentations d'ajouter un remplissage arbitraire avant tout membre de structure autre que le premier. Le code pourrait utiliser sizeof ou offsetof pour s'assurer que les membres de la structure sont placés comme prévu, mais les deux autres problèmes resteraient.

  2. Étant donné quelque chose comme:

    if (structPtr->array1[x])
     structPtr->array2[y]++;
    return structPtr->array1[x];
    

    il serait normalement utile pour un compilateur de supposer que l'utilisation de structPtr->array1[x] donnera la même valeur que l'utilisation précédente dans la condition "if", même si cela modifierait le comportement du code reposant sur un aliasing entre les deux tableaux.

  3. Si array1[] a par exemple 4 éléments, un compilateur étant donné quelque chose comme:

    if (x < 4) foo(x);
    structPtr->array1[x]=1;
    

pourrait en conclure que, puisqu'il n'y aurait pas de cas défini où x ne soit pas inférieur à 4, il pourrait appeler foo(x) sans condition.

Malheureusement, bien que les programmes puissent utiliser sizeof ou offsetof pour s’assurer que l’agencement de structure n’a pas de surprises, ils ne peuvent pas savoir si les compilateurs promettent de s’abstenir de l’optimisation des types 2 ou 3. En outre, la norme est un peu vague sur ce que l'on entend par exemple:

struct foo {char array1[4],array2[4]; };

int test(struct foo *p, int i, int x, int y, int z)
{
  if (p->array2[x])
  {
    ((char*)p)[x]++;
    ((char*)(p->array1))[y]++;
    p->array1[z]++;
  }
  return p->array2[x];
}

La norme est assez claire sur le fait que le comportement ne serait défini que si z est dans la plage 0..3, mais puisque le type de p-> tableau dans cette expression est char * (en raison de la décroissance), la distribution dans l'accès n'est pas claire. utiliser y aurait un effet quelconque. D'autre part, comme convertir un pointeur en premier élément d'une structure en char* devrait donner le même résultat que convertir un pointeur en struct en char*, et que le pointeur struct convert devrait être utilisable pour accéder à tous les octets qu'il contient. x devrait être défini pour (au minimum) x = 0..7 [si le décalage de array2 est supérieur à 4, cela affecterait la valeur de x nécessaire pour frapper les membres de array2, mais une valeur de x pourrait le faire avec défini comportement].

IMHO, un bon remède serait de définir l’opérateur d’indice sur les types de tableau d’une manière qui n’implique pas la décroissance du pointeur. Dans ce cas, les expressions p->array[x] et &(p->array1[x]) peuvent inviter un compilateur à supposer que x est égal à 0..3, mais p->array+x et *(p->array+x) nécessiteraient un compilateur pour permettre la possibilité d'autres valeurs. Je ne sais pas si des compilateurs le font, mais la norme n'en exige pas.

0
supercat