Macro de tamaño de matriz que rechaza punteros

Resuelto nneonneo asked hace 10 años • 10 respuestas

La macro estándar de tamaño de matriz que a menudo se enseña es

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

o alguna formación equivalente. Sin embargo, este tipo de cosas tiene éxito silenciosamente cuando se pasa un puntero y da resultados que pueden parecer plausibles en tiempo de ejecución hasta que las cosas se desmoronan misteriosamente.

Es muy fácil cometer este error: una función que tiene una variable de matriz local se refactoriza, moviendo un poco de manipulación de la matriz a una nueva función llamada con la matriz como parámetro.

Entonces, la pregunta es: ¿existe una macro "sanitaria" para detectar el uso indebido de la ARRAYSIZEmacro en C, preferiblemente en tiempo de compilación? En C++ simplemente usaríamos una plantilla especializada solo para argumentos de matriz; En C, parece que necesitaremos alguna forma de distinguir matrices y punteros. (Si quisiera rechazar matrices, por ejemplo, lo haría, por ejemplo, (arr=arr, ...)porque la asignación de matrices es ilegal).

nneonneo avatar Oct 18 '13 22:10 nneonneo
Aceptado

El kernel de Linux utiliza una buena implementación para ARRAY_SIZEsolucionar este problema:

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

con

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

y

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

Por supuesto, esto es portátil sólo en GNU C ya que utiliza dos elementos intrínsecos: typeofoperador y __builtin_types_compatible_pfunción. También utiliza su "famosa" BUILD_BUG_ON_ZEROmacro que sólo es válida en GNU C.

Suponiendo un requisito de evaluación en tiempo de compilación (que es lo que queremos), no conozco ninguna implementación portátil de esta macro.

Una implementación "semiportátil" (y que no cubriría todos los casos) es:

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

con

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

Con gccesto no se da ninguna advertencia si el argumento es una matriz, -std=c99 -Wallpero -pedanticsí se da una advertencia. La razón es que IS_ARRAYla expresión no es una expresión constante entera (la conversión a tipos de puntero y el operador de subíndice no están permitidos en expresiones constantes enteras) y el ancho del campo de bits STATIC_EXPrequiere una expresión constante entera.

ouah avatar Oct 18 '2013 17:10 ouah

Esta versión devuelve ARRAYSIZE()cuándo 0es arrun puntero y el tamaño cuando es una matriz pura.

#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;
}

Producción:

5
0
10

Como señaló Ben Jackson, puede forzar una excepción de tiempo de ejecución (dividiendo por 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))

Lamentablemente, no se puede forzar un error en tiempo de compilación (la dirección debe argcompararse en tiempo de ejecución)

David Ranieri avatar Oct 18 '2013 15:10 David Ranieri

Con C11, podemos diferenciar matrices y punteros usando _Generic, pero solo encontré una manera de hacerlo si proporcionas el tipo de elemento:

#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 comprueba: 1) el puntero a A no es un puntero a puntero. 2) puntero a elemento es puntero a T. Se evalúa (void)0y falla estáticamente con punteros.

Es una respuesta imperfecta, pero tal vez un lector pueda mejorarla y deshacerse de ese parámetro de tipo.

bluss avatar May 13 '2014 12:05 bluss

Modificación de la respuesta de Bluss usando typeof en lugar de un parámetro de tipo:

#define ARRAY_SIZE(A) \
    _Generic(&(A), \
    typeof((A)[0]) **: (void)0, \
    default: sizeof(A) / sizeof((A)[0]))
4566976 avatar Feb 14 '2015 17:02 4566976