web-dev-qa-db-fra.com

Macro de taille tableau qui rejette les pointeurs

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

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

ou une formation équivalente. Cependant, ce genre de chose réussit en silence 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 est très facile de faire cette erreur: une fonction qui a une variable de tableau locale est refactorisée, ce qui déplace un peu de manipulation de tableau dans une nouvelle fonction appelée avec le tableau en paramètre.

La question est donc la suivante: existe-t-il une macro "sanitaire" permettant de détecter une utilisation incorrecte de la macro ARRAYSIZE en C, de préférence au moment de la compilation? En C++, nous utiliserions simplement un modèle spécialisé uniquement pour les arguments de tableau; en C, il semble que nous aurons besoin d'un moyen de distinguer les tableaux et les pointeurs. (Si je voulais rejeter des tableaux, par exemple, je ne ferais que par exemple (arr=arr, ...) car l’affectation de tableau est illégale).

46
nneonneo

Le noyau Linux utilise une implémentation Nice de ARRAY_SIZE pour résoudre ce problème:

#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, cela n’est portable que dans GNU C car il utilise deux instructions: typeof opérateur et __builtin_types_compatible_p fonction. En outre, il utilise leur "célèbre" macro BUILD_BUG_ON_ZERO qui n'est valide que dans GNU C. 

En supposant une exigence d’évaluation du temps de compilation (ce que nous voulons), je ne connais aucune implémentation portable de cette macro.

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

#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, cela ne donne aucun avertissement si l'argument est un tableau dans -std=c99 -Wall mais -pedantic donnerait un avertissement. La raison en est que IS_ARRAY expression n'est pas une expression constante entière (les types de conversion en pointeur et opérateur d'indice ne sont pas autorisés dans les expressions constantes d'entiers) et la largeur du champ binaire dans STATIC_EXP nécessite une expression constante d'entier.

24
ouah

Cette version de ARRAYSIZE() renvoie 0 lorsque arr est un pointeur et la taille 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 souligné Ben Jackson, vous pouvez forcer une exception d'exécution (en la divisant 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 doit être comparée au moment de l'exécution)

17
Keine Lust

Modification de la réponse de bluss en utilisant typeof au lieu d'un paramètre de type:

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

Avec C11, nous pouvons différencier les tableaux et les pointeurs à l'aide de _Generic, mais je n'ai trouvé le moyen de le faire que si vous fournissez 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) le pointeur sur A n'est pas un pointeur sur un pointeur. 2) pointeur-à-elem est pointeur-à-T. Il est évalué à (void)0 et échoue statiquement avec les pointeurs.

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

5
bluss

En voici un autre qui s'appuie sur l'extension gcctypeof :

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

Cela fonctionne en essayant de configurer un objet identique et en l'initialisant avec un tableau appelé initializer. Si un tableau est passé, le compilateur est content. Si le pointeur est passé, le compilateur se plaint:

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')
2
Digital Trauma

Voici une solution possible utilisant une extension GNU appelée instruction statement :

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

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

Cette solution n'est pas portable sur les plates-formes qui ne prennent pas en charge cette extension GNU. Dans ces cas, je vous recommande simplement d'utiliser la macro standard et de ne pas vous inquiéter de la transmission accidentelle de pointeurs vers la macro.

1
Adam Rosenfield

mon favori personnel, essayé gcc 4.6.3 et 4.9.2:

#define STR_(tokens) # tokens

#define ARRAY_SIZE(array) \
    ({ \
        _Static_assert \
        ( \
            ! __builtin_types_compatible_p(typeof(array), typeof(& array[0])), \
            "ARRAY_SIZE: " STR_(array) " [expanded from: " # array "] is not an array" \
        ); \
        sizeof(array) / sizeof((array)[0]); \
    })

/*
 * example
 */

#define not_an_array ((char const *) "not an array")

int main () {
    return ARRAY_SIZE(not_an_array);
}

impressions du compilateur

x.c:16:12: error: static assertion failed: "ARRAY_SIZE: ((char const *) \"not an array\") [expanded from: not_an_array] is not an array"
0
not-a-user

Un exemple de plus à la collection.

#define LENGTHOF(X) ({ \
    const size_t length = (sizeof X / (sizeof X[0] ?: 1)); \
    typeof(X[0]) (*should_be_an_array)[length] = &X; \
    length; })

Avantages:

  1. Cela fonctionne avec les tableaux normaux, les tableaux de longueur variable, les tableaux multidimensionnels , Les tableaux de structures de taille zéro
  2. Il génère une erreur de compilation (sans avertissement) si vous passez un pointeur, une structure ou une union
  3. Cela ne dépend d'aucune des fonctionnalités de C11
  4. Cela vous donne une erreur très lisible

Les inconvénients:

  1. Cela dépend de certaines des extensions gcc: Typeof , Statement Exprs , et (si vous l'aimez) Conditions
  2. Cela dépend de la fonctionnalité C99 VLA
0
hurufu

Horrible, oui, mais ça marche et c'est portable.

#define ARRAYSIZE(arr) ((sizeof(arr) != sizeof(&arr[0])) ? \
                       (sizeof(arr)/sizeof(*arr)) : \
                       -1+0*fprintf(stderr, "\n\n** pointer in ARRAYSIZE at line %d !! **\n\n", __LINE__))

Cela ne détectera rien au moment de la compilation, mais affichera un message d'erreur dans stderr et retournera -1 s'il s'agit d'un pointeur ou si la longueur du tableau est 1.

==> D&EACUTE;MO <==

0
Michael M.