ti-enxame.com

Macro de tamanho de matriz que rejeita ponteiros

A macro padrão de tamanho de matriz que é frequentemente ensinada é

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

ou alguma formação equivalente. No entanto, esse tipo de coisa sucede silenciosamente quando um ponteiro é passado e fornece resultados que podem parecer plausíveis em tempo de execução até que as coisas caiam misteriosamente.

É muito fácil cometer esse erro: uma função que tem uma variável de matriz local é refatorada, movendo um pouco de manipulação de matriz para uma nova função chamada com o array como um parâmetro.

Portanto, a pergunta é: existe uma macro "sanitária" para detectar o uso indevido da macro ARRAYSIZE em C, preferencialmente em tempo de compilação? Em C++, usamos apenas um modelo especializado apenas para argumentos de matriz; em C, parece que precisamos de alguma maneira de distinguir matrizes e ponteiros. (Se eu quisesse rejeitar matrizes, por exemplo, eu faria, por exemplo, (arr=arr, ...) porque a atribuição de matriz é ilegal).

46
nneonneo

O kernel do Linux usa uma boa implementação do ARRAY_SIZE para lidar com esse problema:

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

com

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

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

É claro que isso é portátil somente em GNU C, já que ele utiliza dois instrinsics: typeof operator e __builtin_types_compatible_p function. Também usa sua macro "famosa" BUILD_BUG_ON_ZERO que só é válida em GNU C. 

Supondo um requisito de avaliação de tempo de compilação (que é o que queremos), não conheço nenhuma implementação portátil dessa macro.

Uma implementação "semi-portátil" (e que não cobriria todos os casos) é:

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

com

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

Com gcc, isso não dá aviso se o argumento for uma matriz em -std=c99 -Wall, mas -pedantic daria um aviso. A razão é que a expressão IS_ARRAY não é uma expressão constante de inteiro (os tipos de conversão para o tipo ponteiro e o operador de subscrito não são permitidos em expressões constantes inteiras) e a largura do campo de bits em STATIC_EXP requer uma expressão constante inteira.

24
ouah

Esta versão de ARRAYSIZE() retorna 0 quando arr é um ponteiro e o tamanho quando é um array puro

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

Saída:

5
0
10

Como apontado por Ben Jackson, você pode forçar uma exceção de tempo de execução (dividindo 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))

Infelizmente, você não pode forçar um erro em tempo de compilação (o endereço de arg deve ser comparado em tempo de execução)

17
Keine Lust

Modificação da resposta do bluss usando typeof em vez de um parâmetro de tipo:

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

Com o C11, podemos diferenciar matrizes e ponteiros usando _Generic, mas eu só encontrei uma maneira de fazer isso se você fornecer o 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));

A macro verifica: 1) ponteiro para A não é ponteiro para ponteiro. 2) ponteiro para elem é ponteiro para T. Ele avalia (void)0 e falha estaticamente com ponteiros.

É uma resposta imperfeita, mas talvez um leitor possa aperfeiçoá-la e se livrar desse parâmetro de tipo!

5
bluss

Aqui está outro que depende da extensão gcctypeof :

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

Isso funciona tentando configurar um objeto idêntico e inicializá-lo com um inicializador designado de matriz. Se uma matriz é passada, o compilador fica feliz. Se o ponteiro é passado, o compilador reclama com:

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

Aqui está uma solução possível usando uma extensão GNUchamada expressões statement :

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

Isto usa uma afirmação static para afirmar que sizeof(arr) != sizeof(void*). Isso tem uma limitação óbvia - você não pode usar essa macro em matrizes cujo tamanho é exatamente um ponteiro (por exemplo, uma matriz de 1 comprimento de ponteiros/inteiros, ou talvez uma matriz de 4 comprimentos de bytes em uma de 32 bits plataforma). Mas essas instâncias particulares podem ser resolvidas com bastante facilidade.

Esta solução não é portável para plataformas que não suportam esta extensão GNU. Nesses casos, eu recomendaria apenas usar a macro padrão e não se preocupar em passar acidentalmente em ponteiros para a macro.

1
Adam Rosenfield

meu favorito pessoal, tentei o gcc 4.6.3 e 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);
}

impressões de compilador

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

Mais um exemplo para a coleção.

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

Prós:

  1. Ele trabalha com arrays normais, arrays de comprimento variável, arrays multidimensionais , Arrays de estruturas de tamanho zero
  2. Ele gera um erro de compilação (sem aviso) se você passar qualquer ponteiro, struct ou Union
  3. Não depende de nenhum dos recursos do C11
  4. Isso lhe dá um erro muito legível

Contras:

  1. Depende de algumas das extensões do gcc: Typeof , Statement Exprs , e (se você gostar) Condicionais
  2. Depende do C99 VLA feature
0
hurufu

Awful, sim, mas isso funciona e é portátil.

#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__))

Isso não detectará nada em tempo de compilação, mas imprimirá uma mensagem de erro em stderr e retornará -1 se for um ponteiro ou se o comprimento da matriz for 1.

==> DEMO <==

0
Michael M.