129 votes

Quelle est l'utilité d'un tableau avec zéro élément ?

Dans le code du noyau Linux, j'ai trouvé la chose suivante que je n'arrive pas à comprendre.

 struct bts_action {
         u16 type;
         u16 size;
         u8 data[0];
 } __attribute__ ((packed));

Le code est ici : http://lxr.free-electrons.com/source/include/linux/ti_wilink_st.h

Quel est le besoin et le but d'un tableau de données avec zéro élément ?

0 votes

Je ne suis pas sûr qu'il doive y avoir soit une tableaux de longueur nulle o struct-hack tag ...

0 votes

@hippietrail, parce que souvent, lorsque quelqu'un demande ce qu'est ce struct, il ne sait pas qu'il est désigné par le terme "flexible array member". S'ils le savaient, ils auraient pu facilement trouver leur réponse. Mais comme il ne le sait pas, il ne peut pas marquer la question comme telle. C'est pourquoi nous ne disposons pas d'une telle balise.

0 votes

Cela pourrait être une raison pour ne pas avoir de balises pour tous les concepts dont certaines personnes ne connaissent pas la terminologie ...

149voto

Shahbaz Points 22525

C'est un moyen d'avoir des tailles de données variables, sans avoir à appeler malloc ( kmalloc dans ce cas) deux fois. Vous l'utiliserez comme ceci :

struct bts_action *var = kmalloc(sizeof(*var) + extra, GFP_KERNEL);

Cela n'était pas standard et était considéré comme un hack (comme l'a dit Aniket), mais c'était normalisé en C99 . Le format standard est désormais le suivant :

struct bts_action {
     u16 type;
     u16 size;
     u8 data[];
} __attribute__ ((packed)); /* Note: the __attribute__ is irrelevant here */

Notez que vous ne mentionnez pas de taille pour les data champ. Notez également que cette variable spéciale ne peut venir qu'à la fin de la structure.


Dans C99, cette question est expliquée en 6.7.2.1.16 (c'est moi qui souligne) :

Dans un cas particulier, le dernier élément d'une structure comportant plus d'un membre nommé peut être le suivant avoir un type de tableau incomplet ; c'est ce qu'on appelle un membre de tableau flexible . Dans la plupart des situations, le membre flexible du tableau est ignoré. En particulier, la taille de la structure est la même que si l'élément membre du tableau flexible était omis, sauf qu'il peut avoir plus de remplissage à la fin que que ce que l'omission impliquerait. Toutefois, lorsqu'un opérateur . (ou ->) a un opérande gauche qui est (un pointeur vers) une structure avec un membre de tableau flexible et que l'opérande droit nomme ce membre de tableau flexible. membre, il se comporte comme si ce membre était remplacé par le plus long tableau (avec le même type d'élément) qui ne rendrait pas la structure plus complexe. même type d'élément) qui ne rendrait pas la structure plus grande que l'objet auquel on accède ; l'offset du tableau reste celui de la structure. le décalage du tableau reste celui du membre du tableau flexible, même si cela diffère de celui du tableau de remplacement. Si ce tableau n'avait pas d'éléments, il se comporterait comme si il avait un élément, mais le comportement est indéfini si l'on tente d'accéder à cet élément ou de le remplacer. élément ou de générer un pointeur sur cet élément.

Ou en d'autres termes, si vous avez :

struct something
{
    /* other variables */
    char data[];
}

struct something *var = malloc(sizeof(*var) + extra);

Vous pouvez accéder à var->data avec des indices dans [0, extra) . Notez que sizeof(struct something) ne donnera que la taille en tenant compte des autres variables, c'est-à-dire qu'elle donne data une taille de 0.


Il peut également être intéressant de noter comment la norme donne effectivement des exemples de malloc d'une telle construction (6.7.2.1.17) :

struct s { int n; double d[]; };

int m = /* some value */;
struct s *p = malloc(sizeof (struct s) + sizeof (double [m]));

Une autre note intéressante de la norme au même endroit est (c'est moi qui souligne) :

en supposant que l'appel à malloc réussisse, l'objet pointé par p se comporte, dans la plupart des cas, comme si p avait été déclaré comme :

struct { int n; double d[m]; } *p;

(il existe des circonstances dans lesquelles cette équivalence est rompue ; en particulier, les offsets du membre d pourraient ne pas être les mêmes ).

0 votes

Pour être clair, le code original dans la question n'est toujours pas standard dans C99 (ni C11), et serait toujours considéré comme un hack. La normalisation en C99 doit omettre le "array bound".

0 votes

Qu'est-ce que [0, extra) ?

2 votes

38voto

Aniket Points 15250

C'est un hack en fait, pour CCG ( C90 ) en fait.

On l'appelle aussi struct hack .

Donc la prochaine fois, je dirais :

struct bts_action *bts = malloc(sizeof(struct bts_action) + sizeof(char)*100);

Cela reviendra à dire :

struct bts_action{
    u16 type;
    u16 size;
    u8 data[100];
};

Et je peux créer un nombre illimité d'objets structurés de ce type.

8voto

sheu Points 3053

L'idée est de permettre un tableau de taille variable à la fin de la structure. On peut le supposer, bts_action est un paquet de données avec un en-tête de taille fixe (le type y size ), et des champs de taille variable data membre. En le déclarant comme un tableau de longueur 0, il peut être indexé comme tout autre tableau. Vous allouez alors un bts_action struct, de disons 1024-byte data la taille, comme ça :

size_t size = 1024;
struct bts_action* action = (struct bts_action*)malloc(sizeof(struct bts_action) + size);

Voir aussi : http://c2.com/cgi/wiki?StructHack

2 votes

@Aniket : Je ne suis pas tout à fait sûr de savoir d'où vient que idée.

0 votes

En C++ oui, en C, pas nécessaire.

3 votes

@sheu, cela vient du fait que votre style d'écriture malloc vous fait vous répéter plusieurs fois et si jamais le type de action change, vous devez le réparer plusieurs fois. Comparez les deux suivants par vous-même et vous saurez : struct some_thing *variable = (struct some_thing *)malloc(10 * sizeof(struct some_thing)); vs. struct some_thing *variable = malloc(10 * sizeof(*variable)); Le second est plus court, plus propre et nettement plus facile à modifier.

7voto

Lundin Points 21616

Le code n'est pas valide C ( voir ceci ). Le noyau Linux n'est, pour des raisons évidentes, pas du tout concerné par la portabilité, il utilise donc beaucoup de code non standard.

Ce qu'ils font est une extension non-standard de GCC avec une taille de tableau de 0. Un programme conforme au standard aurait écrit u8 data[]; et ça aurait signifié la même chose. Les auteurs du noyau Linux aiment apparemment rendre les choses inutilement compliquées et non standard, si une option pour le faire se présente.

Dans les anciens standards C, terminer un struct avec un tableau vide était connu sous le nom de "the struct hack". D'autres ont déjà expliqué son but dans d'autres réponses. Le struct hack, dans la norme C90, était un comportement non défini et pouvait provoquer des plantages, principalement parce qu'un compilateur C est libre d'ajouter un nombre quelconque d'octets de remplissage à la fin du struct. Ces octets de remplissage peuvent entrer en collision avec les données que vous avez essayé de "hacker" à la fin du struct.

GCC a très tôt fait une extension non standard pour passer d'un comportement indéfini à un comportement bien défini. La norme C99 a ensuite adapté ce concept et tout programme C moderne peut donc utiliser cette fonctionnalité sans risque. Elle est connue sous le nom de élément de réseau flexible dans C99/C11.

3 votes

Je doute que "le noyau linux ne soit pas concerné par la portabilité". Peut-être vouliez-vous dire la portabilité vers d'autres compilateurs ? Il est vrai qu'elle est assez liée aux caractéristiques de gcc.

3 votes

Néanmoins, je pense que ce morceau de code particulier n'est pas un code courant et est probablement laissé de côté parce que son auteur n'y a pas prêté beaucoup d'attention. La licence indique qu'il s'agit de pilotes texas instruments, il est donc peu probable que les programmeurs du noyau y aient prêté attention. Je suis presque sûr que les développeurs du noyau mettent constamment à jour l'ancien code en fonction des nouveaux standards ou des nouvelles optimisations. C'est juste trop gros pour s'assurer que tout est mis à jour !

2 votes

@Shahbaz Avec la partie "évidente", je voulais dire la portabilité vers d'autres systèmes d'exploitation, ce qui n'aurait naturellement aucun sens. Mais ils ne semblent pas se soucier de la portabilité vers d'autres compilateurs non plus, ils ont utilisé tellement d'extensions GCC que Linux ne sera probablement jamais porté vers un autre compilateur.

2voto

Wei Shen Points 1138

Une autre utilisation du tableau de longueur zéro est une étiquette nommée à l'intérieur d'une structure pour faciliter la vérification du décalage de la structure au moment de la compilation.

Supposons que vous ayez de grandes définitions de structures (s'étendant sur plusieurs lignes de cache) et que vous vouliez vous assurer qu'elles sont alignées sur la limite de la ligne de cache à la fois au début et au milieu, là où elles traversent la limite.

struct example_large_s
{
    u32 first; // align to CL
    u32 data;
    ....
    u64 *second;  // align to second CL after the first one
    ....
};

Dans le code, vous pouvez les déclarer en utilisant des extensions GCC comme :

__attribute__((aligned(CACHE_LINE_BYTES)))

Mais il faut quand même s'assurer que cela est appliqué au moment de l'exécution.

ASSERT (offsetof (example_large_s, first) == 0);
ASSERT (offsetof (example_large_s, second) == CACHE_LINE_BYTES);

Cela fonctionnerait pour une seule structure, mais il serait difficile de couvrir plusieurs structures, chacune ayant un nom de membre différent à aligner. Vous obtiendrez probablement un code comme celui ci-dessous où vous devez trouver les noms du premier membre de chaque structure :

assert (offsetof (one_struct,     <name_of_first_member>) == 0);
assert (offsetof (one_struct,     <name_of_second_member>) == CACHE_LINE_BYTES);
assert (offsetof (another_struct, <name_of_first_member>) == 0);
assert (offsetof (another_struct, <name_of_second_member>) == CACHE_LINE_BYTES);

Au lieu de procéder de cette manière, vous pouvez déclarer un tableau de longueur nulle dans la structure qui agit comme une étiquette nommée avec un nom cohérent mais qui ne consomme pas d'espace.

#define CACHE_LINE_ALIGN_MARK(mark) u8 mark[0] __attribute__((aligned(CACHE_LINE_BYTES)))
struct example_large_s
{
    CACHE_LINE_ALIGN_MARK (cacheline0);
    u32 first; // align to CL
    u32 data;
    ....
    CACHE_LINE_ALIGN_MARK (cacheline1);
    u64 *second;  // align to second CL after the first one
    ....
};

Le code d'assertion d'exécution serait alors beaucoup plus facile à maintenir :

assert (offsetof (one_struct,     cacheline0) == 0);
assert (offsetof (one_struct,     cacheline1) == CACHE_LINE_BYTES);
assert (offsetof (another_struct, cacheline0) == 0);
assert (offsetof (another_struct, cacheline1) == CACHE_LINE_BYTES);

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X