67 votes

Macro de taille de tableau qui rejette les pointeurs

La macro standard de taille de tableau qui est souvent enseignée est la suivante

#define ARRAYSIZE(arr) (sizeof(arr) / sizeof(arr[0]))

ou toute autre formation équivalente. Cependant, ce genre de chose réussit silencieusement lorsqu'un pointeur est passé, et donne des résultats qui peuvent sembler plausibles à l'exécution jusqu'à ce que les choses s'effondrent mystérieusement.

Il n'est que trop facile de commettre cette erreur : une fonction qui possède une variable locale de type tableau est remaniée, déplaçant une partie de la manipulation du tableau dans une nouvelle fonction appelée avec le tableau en tant que paramètre.

La question est donc de savoir s'il existe une macro "sanitaire" pour détecter l'utilisation abusive de la ARRAYSIZE en C, de préférence au moment de la compilation ? En C++, nous utiliserions simplement un modèle spécialisé pour les arguments de type tableau uniquement ; en C, il semble que nous ayons besoin d'un moyen de distinguer les tableaux et les pointeurs. (Si je voulais rejeter les tableaux, par exemple, je ferais simplement e.g. (arr=arr, ...) car l'assignation d'un tableau est illégale).

1 votes

Cela va être difficile, car les tableaux se transforment en pointeurs dans pratiquement tous les contextes.

1 votes

Pourquoi quelqu'un aurait-il besoin d'une telle macro ? Cela ne fonctionne qu'avec des tableaux qui ont été définis par une taille fixe dans le code, pourquoi auriez-vous besoin de calculer ce que vous savez avoir écrit ? Si la réponse est "peut-être que vous êtes dans une autre partie de votre code et que vous n'avez plus cette information", ma question suivante est : comment est-ce possible avec le tableau qui ne se décompose pas en pointeur, dans un morceau de code non étrange et non spécifiquement conçu pour que cela se produise ?

1 votes

@Eregrith:cela fonctionne aussi avec les VLA, les réseaux de longueur variable. Mais c'est un détail. Cela "ne fonctionne qu'avec les tableaux dont la définition est dans le champ d'application" est plus proche de la réalité, mais pas très différent de ce que vous avez dit.

46voto

ouah Points 75311

Le noyau Linux utilise une bonne implémentation de ARRAY_SIZE pour traiter cette question :

#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]) + __must_be_array(arr))

avec

#define __must_be_array(a) BUILD_BUG_ON_ZERO(__same_type((a), &(a)[0]))

et

#define __same_type(a, b) __builtin_types_compatible_p(typeof(a), typeof(b))

Bien sûr, cette méthode n'est portable que dans GNU C, car elle utilise t typeof et __builtin_types_compatible_p fonction. Il utilise également leur "célèbre" BUILD_BUG_ON_ZERO qui n'est valable qu'en GNU C.

Dans l'hypothèse d'une exigence d'évaluation à la compilation (ce que nous voulons), je ne connais pas d'implémentation portable de cette macro.

Une implémentation "semi-portable" (et qui ne couvrirait pas tous les cas) est la suivante :

#define ARRAY_SIZE(arr)  \
    (sizeof(arr) / sizeof((arr)[0]) + STATIC_EXP(IS_ARRAY(arr)))

avec

#define IS_ARRAY(arr)  ((void*)&(arr) == &(arr)[0])
#define STATIC_EXP(e)  \
    (0 * sizeof (struct { int ARRAY_SIZE_FAILED:(2 * (e) - 1);}))

Avec gcc ne donne pas d'avertissement si l'argument est un tableau dans -std=c99 -Wall mais -pedantic émettrait un avertissement. La raison en est la suivante IS_ARRAY n'est pas une expression constante entière (le cast vers les types de pointeurs et l'opérateur d'indice ne sont pas autorisés dans les expressions constantes entières) et la largeur du champ de bits dans l'expression STATIC_EXP nécessite une expression constante entière.

2 votes

Oh, joli, c'est un bijou. J'aurais dû me douter que les développeurs du noyau Linux s'en chargeraient.

20voto

Alter Mann Points 10564

Cette version de ARRAYSIZE() retours 0 cuando arr est un pointeur et la taille lorsqu'il s'agit d'un tableau pur

#include <stdio.h>

#define IS_INDEXABLE(arg) (sizeof(arg[0]))
#define IS_ARRAY(arg) (IS_INDEXABLE(arg) && (((void *) &arg) == ((void *) arg)))
#define ARRAYSIZE(arr) (IS_ARRAY(arr) ? (sizeof(arr) / sizeof(arr[0])) : 0)

int main(void)
{
    int a[5];
    int *b = a;
    int n = 10;
    int c[n]; /* a VLA */

    printf("%zu\n", ARRAYSIZE(a));
    printf("%zu\n", ARRAYSIZE(b));
    printf("%zu\n", ARRAYSIZE(c));
    return 0;
}

Sortie :

5
0
10

Comme l'a fait remarquer Ben Jackson, vous pouvez forcer une exception d'exécution (division par 0)

#define IS_INDEXABLE(arg) (sizeof(arg[0]))
#define IS_ARRAY(arg) (IS_INDEXABLE(arg) && (((void *) &arg) == ((void *) arg)))
#define ARRAYSIZE(arr) (sizeof(arr) / (IS_ARRAY(arr) ? sizeof(arr[0]) : 0))

Malheureusement, vous ne pouvez pas forcer une erreur de compilation (l'adresse de arg doivent être comparés au moment de l'exécution)

1 votes

Mieux encore, il serait possible d'obtenir une erreur au moment de la compilation (diviser par 0 ?) dans le mauvais cas.

0 votes

Quel est le besoin de IS_INDEXABLE(arg) ? Pour autant que je puisse en juger, cette méthode renvoie toujours un résultat non nul

3 votes

@DigitalTrauma, Parce qu'il soulève une erreur lorsque l'argument n'est pas un tableau (ou un pointeur). error: subscripted value is neither array nor pointer nor vector

6voto

user139873 Points 86

En C11, nous pouvons différencier les tableaux et les pointeurs à l'aide de la fonction _Generic mais je n'ai trouvé un moyen de le faire que si l'on fournit le type d'élément :

#define ARRAY_SIZE(A, T) \
    _Generic(&(A), \
            T **: (void)0, \
            default: _Generic(&(A)[0], T *: sizeof(A) / sizeof((A)[0])))

int a[2];
printf("%zu\n", ARRAY_SIZE(a, int));

La macro vérifie : 1) pointeur-à-A n'est pas pointeur-à-pointeur. 2) pointeur-à-élément est pointeur-à-T. Elle s'évalue à (void)0 et échoue statiquement avec les pointeurs.

C'est une réponse imparfaite, mais peut-être qu'un lecteur pourra l'améliorer et se débarrasser de ce paramètre de type !

0 votes

Au lieu de vérifier que "pointeur-à-A n'est pas pointeur-à-pointeur", pourquoi ne pas vérifier directement que pointeur-à-A est pointeur-à-réseau ? _Generic(&(A), T(*)[sizeof(A) / sizeof((A)[0])]: sizeof(A) / sizeof((A)[0])) Cela rend le deuxième test inutile et je pense que le message d'erreur error: '_Generic' selector of type 'int **' is not compatible with any association est plus compréhensible que error: invalid use of void expression . Malheureusement, je n'ai toujours pas d'idée sur la façon de se débarrasser de ce type de paramètre :-(

0 votes

Si vous pouvez transmettre le type d'élément, c'est en fait assez facile. #define ARRAYSIZE(arr, T) _Generic(&(arr), T(*)[sizeof(arr)/sizeof(arr[0])]: sizeof(arr)/sizeof(arr[0])) Cela crée un pointeur de tableau vers un tableau du type spécifié. Si le paramètre passé n'est pas un tableau du bon type ou de la bonne taille, vous obtiendrez des erreurs de compilation. 100% portable en C standard.

1voto

DigitalTrauma Points 6235

En voici un autre qui s'appuie sur la fonction gcc typeof extension :

#define ARRAYSIZE(arr) ({typeof (arr) arr ## _is_a_pointer __attribute__((unused)) = {}; \
                         sizeof(arr) / sizeof(arr[0]);})

Cela fonctionne en essayant de créer un objet identique et de l'initialiser avec un initialisateur désigné par un tableau. Si un tableau est passé, le compilateur est content. Si un pointeur est passé, le compilateur se plaint avec :

arraysize.c: In function 'main':
arraysize.c:11: error: array index in non-array initializer
arraysize.c:11: error: (near initialization for 'p_is_a_pointer')

0 votes

C'est bien ! En fait, cela fonctionne mieux si vous utilisez = {}; Si vous passez un pointeur, vous obtenez "empty scalar initializer". Cela rend le système portable, par exemple, pour les tableaux structurés.

0 votes

@nneonneo - = {}; n'a pas fonctionné pour moi :( - si je passe un simple tableau d'int, j'obtiens aussi "error : empty scalar initializer". Mais je peux passer des tableaux d'ints, des tableaux de pointeurs ou des tableaux de structures à la fonction = 0; sans difficulté.

0 votes

Le {[0] = 0} produit cependant quelques avertissements à propos d'accolades manquantes si vous avez un tableau de tableaux ou un tableau de structures.

1voto

Adam Rosenfield Points 176408

Voici une solution possible utilisant une extension GNU appelée expressions de l'énoncé :

#define ARRAYSIZE(arr) \
    ({typedef char ARRAYSIZE_CANT_BE_USED_ON_POINTERS[sizeof(arr) == sizeof(void*) ? -1 : 1]; \
     sizeof(arr) / sizeof((arr)[0]);})

Il s'agit d'un assertion statique d'affirmer que sizeof(arr) != sizeof(void*) . Ceci a une limitation évidente : vous ne pouvez pas utiliser cette macro sur des tableaux dont la taille est exactement un pointeur (par exemple, un tableau d'une longueur de pointeurs/entiers, ou peut-être un tableau de 4 longueurs d'octets sur une plate-forme 32 bits). Mais ces cas particuliers peuvent être contournés assez facilement.

Cette solution n'est pas portable sur les plateformes qui ne supportent pas cette extension GNU. Dans ce cas, je recommande d'utiliser la macro standard et de ne pas s'inquiéter de passer accidentellement des pointeurs vers la macro.

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