Que se passe-t-il si je définis un tableau de taille 0 en C / C ++?

127

Juste curieux, que se passe-t-il réellement si je définis un tableau de longueur nulle int array[0];dans le code? GCC ne se plaint pas du tout.

Exemple de programme

#include <stdio.h>

int main() {
    int arr[0];
    return 0;
}

Clarification

J'essaie en fait de déterminer si les tableaux de longueur nulle initialisés de cette façon, au lieu d'être pointés comme la longueur variable dans les commentaires de Darhazer, sont optimisés ou non.

C'est parce que je dois publier du code dans la nature, donc j'essaie de déterminer si je dois gérer les cas où le SIZEest défini comme 0, ce qui se produit dans un code avec une définition statiqueint array[SIZE];

J'ai été en fait surpris que GCC ne se plaint pas, ce qui a conduit à ma question. D'après les réponses que j'ai reçues, je pense que l'absence d'avertissement est en grande partie due au support de l'ancien code qui n'a pas été mis à jour avec la nouvelle syntaxe [].

Parce que je m'interrogeais principalement sur l'erreur, je marque la réponse de Lundin comme correcte (Nawaz était le premier, mais ce n'était pas aussi complet) - les autres soulignaient son utilisation réelle pour les structures rembourrées par la queue, bien que pertinente, n'est pas t exactement ce que je cherchais.

Alex Koay
la source
51
@AlexanderCorwin: Malheureusement, en C ++, avec un comportement non défini, des extensions non standard et d'autres anomalies, essayer quelque chose par vous-même n'est souvent pas un chemin vers la connaissance.
Benjamin Lindley
5
@JustinKirk Je viens de me faire piéger par cela aussi en testant et en voyant que cela fonctionnait. Et en raison des critiques que j'ai reçues dans mon message, j'ai appris que le tester et le voir fonctionner ne signifie pas qu'il est valide et légal. Un autotest n'est donc parfois pas valide.
StormByte
2
@JustinKirk, voyez la réponse de Matthieu pour un exemple de l'endroit où vous l'utiliseriez. Cela peut également être utile dans un modèle où la taille du tableau est un paramètre de modèle. L'exemple de la question est évidemment hors contexte.
Mark Ransom
2
@JustinKirk: À quoi sert []en Python ou même ""en C? Parfois, vous avez une fonction ou une macro qui nécessite un tableau, mais vous n'avez aucune donnée à y mettre.
dan04
15
Qu'est-ce que "C / C ++"? Ce sont deux langues distinctes
Courses de légèreté en orbite

Réponses:

86

Un tableau ne peut pas avoir une taille nulle.

ISO 9899: 2011 6.7.6.2:

Si l'expression est une expression constante, elle doit avoir une valeur supérieure à zéro.

Le texte ci-dessus est vrai à la fois pour un tableau simple (paragraphe 1). Pour un VLA (variable length array), le comportement n'est pas défini si la valeur de l'expression est inférieure ou égale à zéro (paragraphe 5). Il s'agit d'un texte normatif dans le standard C. Un compilateur n'est pas autorisé à l'implémenter différemment.

gcc -std=c99 -pedantic donne un avertissement pour le cas non VLA.

Lundin
la source
34
"il doit effectivement donner une erreur" - la distinction entre "avertissements" et "erreurs" n'est pas reconnue dans la norme (elle ne mentionne que les "diagnostics"), et la seule situation où la compilation doit s'arrêter [c'est-à-dire la différence du monde réel entre avertissement et erreur] est sur la rencontre d'une #errordirective.
Random832
12
Pour info, en règle générale, les standards (C ou C ++) indiquent uniquement ce que les compilateurs doivent autoriser , mais pas ce qu'ils doivent interdire . Dans certains cas, ils indiqueront que le compilateur doit émettre un "diagnostic", mais c'est à peu près aussi spécifique qu'ils le reçoivent. Le reste est laissé au fournisseur du compilateur. EDIT: Ce que Random832 a dit aussi.
mcmcc
8
@Lundin "Un compilateur n'est pas autorisé à construire un binaire contenant des tableaux de longueur nulle." La norme ne dit absolument rien de tel. Il indique seulement qu'il doit générer au moins un message de diagnostic lorsqu'il reçoit un code source contenant un tableau avec une expression constante de longueur nulle pour sa taille. La seule circonstance dans laquelle la norme interdit à un compilateur de construire un binaire est s'il rencontre une #errordirective de préprocesseur.
Random832
5
@Lundin Générer un binaire pour tous les cas corrects satisfait le n ° 1, et en générer ou non un pour les cas incorrects ne l'affecterait pas. L'impression d'un avertissement suffit pour # 3. Ce comportement n'a aucune pertinence pour # 2, puisque la norme ne définit pas le comportement de ce code source.
Random832
13
@Lundin: Le fait est que votre déclaration est erronée; les compilateurs conformes sont autorisés à construire un binaire contenant des tableaux de longueur nulle, tant qu'un diagnostic est émis.
Keith Thompson
85

Selon la norme, ce n'est pas autorisé.

Cependant, il est courant dans les compilateurs C de traiter ces déclarations comme une déclaration de membre de tableau flexible ( FAM ) :

C99 6.7.2.1, §16 : comme cas particulier, le dernier élément d'une structure avec plus d'un membre nommé peut avoir un type tableau incomplet; c'est ce qu'on appelle un membre de tableau flexible.

La syntaxe standard d'un FAM est:

struct Array {
  size_t size;
  int content[];
};

L'idée est que vous l'attribueriez alors ainsi:

void foo(size_t x) {
  Array* array = malloc(sizeof(size_t) + x * sizeof(int));

  array->size = x;
  for (size_t i = 0; i != x; ++i) {
    array->content[i] = 0;
  }
}

Vous pouvez également l'utiliser de manière statique (extension gcc):

Array a = { 3, { 1, 2, 3 } };

Ceci est également connu sous le nom de structures rembourrées en queue (ce terme est antérieur à la publication de la norme C99) ou de struct hack (merci à Joe Wreschnig de l'avoir signalé).

Cependant cette syntaxe n'a été normalisée (et les effets garantis) que récemment en C99. Avant, une taille constante était nécessaire.

  • 1 était la voie portable à suivre, même si c'était plutôt étrange.
  • 0 était meilleur pour indiquer l'intention, mais pas légal en ce qui concerne la norme et soutenu en tant qu'extension par certains compilateurs (y compris gcc).

Cependant, la pratique du rembourrage de la queue repose sur le fait que le stockage est disponible (prudent malloc) et n'est donc pas adapté à l'utilisation de la pile en général.

Matthieu M.
la source
@Lundin: Je n'ai vu aucun VLA ici, toutes les tailles sont connues au moment de la compilation. Le terme de tableau flexible vient de gcc.gnu.org/onlinedocs/gcc-4.1.2/gcc/Zero-Length.html et se qualifie int content[];ici pour autant que je sache . Puisque je ne suis pas trop averti sur les termes C de l'art ... pouvez-vous confirmer si mon raisonnement semble correct?
Matthieu M.
@MatthieuM .: C99 6.7.2.1, §16: A titre de cas particulier, le dernier élément d'une structure avec plus d'un membre nommé peut avoir un type tableau incomplet; c'est ce qu'on appelle un membre de tableau flexible.
Christoph
Cet idiome est également connu sous le nom de "struct hack" , et j'ai rencontré plus de gens familiers avec ce nom que "tail-padded structure" (jamais entendu auparavant, sauf peut-être comme une référence générique au remplissage d'une structure pour une future compatibilité ABI ) ou "flexible array member" que j'ai entendu pour la première fois en C99.
1
Utiliser une taille de tableau de 1 pour le piratage de structure éviterait que les compilateurs sifflent, mais n'était que "portable" parce que les rédacteurs de compilateurs étaient assez gentils pour reconnaître cette utilisation comme un standard de facto. S'il n'y avait pas l'interdiction des tableaux de taille nulle, l'utilisation conséquente par les programmeurs de tableaux à un seul élément comme substitut minable, et l'attitude historique des rédacteurs du compilateur qu'ils devraient servir les besoins du programmeur même lorsqu'ils ne sont pas requis par le Standard, les rédacteurs de compilateurs pourraient ont facilement et utilement optimiséfoo[x] à foo[0]chaque fois qu'il foos'agissait d'un tableau à un seul élément.
supercat
1
@RobertSsupportsMonicaCellio: comme indiqué explicitement dans la réponse, mais à la fin . J'ai également chargé l'explication à l'avant, pour la rendre plus claire dès le départ.
Matthieu M.
58

En C et C ++ standard, un tableau de taille nulle n'est pas autorisés.

Si vous utilisez GCC, compilez-le avec l' -pedanticoption. Il donnera un avertissement en disant:

zero.c:3:6: warning: ISO C forbids zero-size array 'a' [-pedantic]

Dans le cas de C ++, il donne un avertissement similaire.

Nawaz
la source
9
Dans Visual C ++ 2010:error C2466: cannot allocate an array of constant size 0
Mark Ransom
4
-Werror transforme simplement tous les avertissements en erreurs, cela ne corrige pas le comportement incorrect du compilateur GCC.
Lundin
C ++ Builder 2009 donne également correctement une erreur:[BCC32 Error] test.c(3): E2021 Array must have at least one element
Lundin
1
Au lieu de -pedantic -Werror, vous pouvez aussi simplement faire-pedantic-errors
Stephan Dollberg
3
Un tableau de taille zéro n'est pas tout à fait la même chose qu'un tableau de taille zéro std::array. (À part: je me souviens, mais je ne peux pas trouver la source que les VLA ont été considérés et explicitement rejetés d'être en C ++.)
27

C'est totalement illégal, et l'a toujours été, mais beaucoup de compilateurs négligent de signaler l'erreur. Je ne sais pas pourquoi vous voulez faire ça. La seule utilisation que je connais est de déclencher une erreur de compilation à partir d'un booléen:

char someCondition[ condition ];

Si conditionest un faux, alors j'obtiens une erreur de compilation. Parce que les compilateurs le permettent, cependant, j'ai commencé à utiliser:

char someCondition[ 2 * condition - 1 ];

Cela donne une taille de 1 ou -1, et je n'ai jamais trouvé de compilateur qui accepterait une taille de -1.

James Kanze
la source
C'est un hack intéressant pour l'utiliser.
Alex Koay
10
C'est une astuce courante en métaprogrammation, je pense. Je ne serais pas surpris si les implémentations de l' STATIC_ASSERTutilisaient.
James Kanze
Pourquoi pas simplement:#if condition \n #error whatever \n #endif
Jerfov2
1
@ Jerfov2 car la condition peut ne pas être connue au moment du prétraitement, uniquement au moment de la compilation
rmeador
9

J'ajouterai qu'il y a une page entière de la documentation en ligne de gcc sur cet argument.

Quelques citations:

Les tableaux de longueur nulle sont autorisés dans GNU C.

Dans l'ISO C90, vous devrez donner au contenu une longueur de 1

et

Les versions de GCC antérieures à 3.0 permettaient d'initialiser statiquement les tableaux de longueur nulle, comme s'il s'agissait de tableaux flexibles. En plus des cas qui étaient utiles, il permettait également les initialisations dans des situations qui corrompraient les données ultérieures

pour que tu puisses

int arr[0] = { 1 };

et boum :-)

xanatos
la source
Puis-je faire comme int a[0], alors a[0] = 1 a[1] = 2??
Suraj Jain
2
@SurajJain Si vous voulez écraser votre pile :-) C ne vérifie pas l'index par rapport à la taille du tableau que vous écrivez, donc vous pouvez a[100000] = 5mais si vous êtes chanceux, vous planterez simplement votre application, si vous êtes chanceux: -)
xanatos
Int a [0]; signifie un tableau variable (tableau de taille zéro), Comment puis-je l'attribuer maintenant
Suraj Jain
@SurajJain Quelle partie de "C ne vérifie pas l'index par rapport à la taille du tableau que vous écrivez" n'est pas claire? Il n'y a pas de vérification d'index en C, vous pouvez écrire après la fin du tableau et planter l'ordinateur ou écraser des bits précieux de votre mémoire. Donc, si vous avez un tableau de 0 éléments, vous pouvez écrire après la fin des 0 éléments.
xanatos
Voir cette quora.com
Suraj Jain
9

Une autre utilisation des tableaux de longueur nulle est de créer des objets de longueur variable (pré-C99). Tableaux de longueur zéro sont différents de réseaux flexibles qui ont [] sans 0.

Cité de gcc doc :

Les tableaux de longueur nulle sont autorisés dans GNU C. Ils sont très utiles comme dernier élément d'une structure qui est vraiment un en-tête pour un objet de longueur variable:

 struct line {
   int length;
   char contents[0];
 };
 
 struct line *thisline = (struct line *)
   malloc (sizeof (struct line) + this_length);
 thisline->length = this_length;

Dans ISO C99, vous utiliseriez un membre de tableau flexible, qui est légèrement différent en syntaxe et en sémantique:

  • Les membres de tableau flexibles sont écrits en tant que contenus [] sans le 0.
  • Les membres du tableau flexible ont un type incomplet, et donc l'opérateur sizeof peut ne pas être appliqué.

Un exemple concret est celui des tableaux de longueur nulle de struct kdbus_itemdans kdbus.h (un module du noyau Linux).

Duc
la source
2
À mon humble avis, il n'y avait aucune bonne raison pour que la norme ait interdit les tableaux de longueur nulle; il pourrait avoir des objets de taille zéro très bien en tant que membres d'une structure et les considérer comme void*à des fins d'arithmétique (donc ajouter ou soustraire des pointeurs à des objets de taille zéro serait interdit). Alors que les membres de tableau flexible sont généralement meilleurs que les tableaux de taille zéro, ils peuvent également agir comme une sorte d '"union" pour alias des choses sans ajouter un niveau supplémentaire d'indirection "syntaxique" à ce qui suit (par exemple, étant donné que l' struct foo {unsigned char as_bytes[0]; int x,y; float z;}on peut accéder aux membres x... z...
supercat
... directement sans avoir à dire, par exemple myStruct.asFoo.x, etc. contenu.
supercat du
@supercat une bonne raison est de maintenir l'intégrité de la règle d'accès aux limites du tableau en dehors. En tant que dernier membre d'une structure, le membre de tableau flexible C99 produit exactement le même effet que le tableau de taille zéro GCC, mais sans avoir besoin d'ajouter des cas particuliers aux autres règles. À mon humble avis, c'est une amélioration qui sizeof x->contentsest une erreur dans ISO C au lieu de renvoyer 0 dans gcc. Les tableaux de taille zéro qui ne sont pas des membres de structure présentent un tas d'autres problèmes.
MM du
@MM: Quels problèmes causeraient-ils si la soustraction de deux pointeurs égaux à un objet de taille zéro était définie comme donnant zéro (comme le ferait la soustraction de pointeurs égaux à n'importe quelle taille d'objet), et la soustraction de pointeurs inégaux à des objets de taille zéro était définie comme produisant Valeur non spécifiée? Si le Standard avait spécifié qu'une implémentation peut permettre à une structure contenant un FAM d'être incorporée dans une autre structure à condition que l'élément suivant dans cette dernière structure soit soit un tableau avec le même type d'élément que le FAM, soit une structure commençant par un tel tableau , et à condition que ...
supercat
... il reconnaît le FAM comme alias du tableau (si les règles d'alignement entraînaient un atterrissage des tableaux à différents décalages, un diagnostic serait nécessaire), cela aurait été très utile. Dans l'état actuel des choses, il n'y a pas de bon moyen d'avoir une méthode qui accepte des pointeurs vers des structures du format général struct {int n; THING dat[];}et puisse travailler avec des choses de durée statique ou automatique.
supercat le
6

Les déclarations de tableau de taille zéro dans les structures seraient utiles si elles étaient autorisées, et si la sémantique était telle que (1) elles forceraient l'alignement mais n'alloueraient pas d'espace, et (2) l'indexation du tableau serait considérée comme un comportement défini dans le cas où le pointeur résultant serait dans le même bloc de mémoire que la structure. Un tel comportement n'a jamais été autorisé par aucun standard C, mais certains compilateurs plus anciens l'ont autorisé avant qu'il ne devienne standard pour les compilateurs d'autoriser les déclarations de tableau incomplètes avec des crochets vides.

Le piratage de structure, tel qu'il est généralement implémenté en utilisant un tableau de taille 1, est douteux et je ne pense pas qu'il soit nécessaire que les compilateurs s'abstiennent de le casser. Par exemple, je m'attendrais à ce que si un compilateur voit int a[1], il serait dans ses droits de considérer a[i]commea[0] . Si quelqu'un essaie de contourner les problèmes d'alignement du hack struct via quelque chose comme

typedef struct {
  taille uint32_t;
  données uint8_t [4]; // Utilisez quatre, pour éviter que le remplissage ne rejette la taille de la structure
}

un compilateur peut devenir intelligent et supposer que la taille du tableau est en réalité de quatre:

; Comme écrit
  toto = myStruct-> data [i];
; Telle qu'interprétée (en supposant un matériel petit-boutiste)
  toto = ((* (uint32_t *) monStruct-> données) >> (i << 3)) & 0xFF;

Une telle optimisation pourrait être raisonnable, surtout si elle myStruct->datapouvait être chargée dans un registre dans la même opération que myStruct->size. Je ne sais rien dans la norme qui interdirait une telle optimisation, même si bien sûr cela briserait tout code qui pourrait s'attendre à accéder à des éléments au-delà du quatrième élément.

supercat
la source
1
Le membre flexible du tableau été ajouté à C99 en tant que version légitime du hack struct
MM
La norme dit que les accès aux différents membres du tableau ne sont pas en conflit, ce qui aurait tendance à rendre cette optimisation impossible.
Ben Voigt
@BenVoigt: Le standard du langage C ne spécifie pas l'effet d'écrire un octet et de lire le contenant un mot simultanément, mais 99,9% des processeurs spécifient que l'écriture réussira et que le mot contiendra soit la nouvelle ou l'ancienne version du byte avec le contenu non modifié des autres octets. Si un compilateur cible de tels processeurs, quel serait le conflit?
supercat
@supercat: le standard du langage C garantit que les écritures simultanées sur deux éléments de tableau différents ne seront pas en conflit. Donc, votre argument selon lequel (lire pendant l'écriture) fonctionne bien, n'est pas suffisant.
Ben Voigt
@BenVoigt: Si un morceau de code devait, par exemple, écrire dans les éléments de tableau 0, 1 et 2 dans une certaine séquence, il ne serait pas autorisé à lire les quatre éléments en un long, en modifier trois et à réécrire les quatre, mais je pense qu'il serait permis de lire les quatre dans un long, de modifier trois, de réécrire les 16 bits inférieurs comme un court et les bits 16-23 comme un octet. Êtes-vous en désaccord avec cela? Et le code qui n'avait besoin que de lire des éléments du tableau serait autorisé à les lire simplement dans un long et à l'utiliser.
supercat du