web-dev-qa-db-ja.com

ポインタを拒否する配列サイズのマクロ

よく教えられている標準の配列サイズのマクロは

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

または同等のフォーメーション。ただし、この種のことは、ポインターが渡されたときに暗黙のうちに成功し、不思議なことにバラバラになるまで、実行時にもっともらしい結果が得られます。

この間違いを犯すのはとても簡単です。ローカル配列変数を持つ関数はリファクタリングされ、配列操作をビットとしてパラメータとして呼び出される新しい関数に移動します。

したがって、問題は、CのARRAYSIZEマクロの誤用を、できればコンパイル時に検出するための「衛生的な」マクロがあるかどうかです。 C++では、配列引数のみに特化したテンプレートを使用します。 Cでは、配列とポインタを区別する方法が必要になるようです。 (たとえば、配列を拒否したい場合は、たとえば(arr=arr, ...)配列の割り当てが無効であるため)。

54
nneonneo

Linuxカーネルは、この問題に対処するためにARRAY_SIZEのNice実装を使用しています。

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

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

もちろん、これはGNU Cでのみ移植可能です。これは、2つの組み込み関数:typeof演算子と__builtin_types_compatible_p関数を使用するためです。また、 "有名な" BUILD_BUG_ON_ZEROマクロはGNU Cでのみ有効です。

コンパイル時の評価要件(これは私たちが望むことです)を想定すると、このマクロの移植可能な実装はわかりません。

「セミポータブル」な実装(すべてのケースをカバーするわけではありません)は次のとおりです。

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

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

gccを使用すると、引数が-std=c99 -Wallの配列の場合は警告が表示されませんが、-pedanticは警告を表示します。理由は、IS_ARRAY式が整数定数式ではなく(ポインター型へのキャストと添字演算子は整数定数式では許可されない)、STATIC_EXPのビットフィールド幅には整数定数式が必要です。

29
ouah

このバージョンのARRAYSIZE()は、arrがポインターの場合は0を返し、純粋な配列の場合はサイズを返します

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

出力:

5
0
10

ベン・ジャクソンが指摘したように、ランタイム例外を強制できます(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))

悲しいことに、コンパイル時のエラーを強制することはできません(argのアドレスは実行時に比較する必要があります)

17
David Ranieri

型パラメーターの代わりにtypeofを使用したブラスの回答の変更:

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

C11では、_Genericを使用して配列とポインターを区別できますが、エレメントタイプを指定した場合にのみ、その方法を見つけました。

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

マクロは以下をチェックします。1)Aへのポインターはポインターへのポインターではありません。 2)pointer-to-elemはpointer-to-Tです。 (void)0と評価され、ポインターで静的に失敗します。

それは不完全な答えですが、おそらく読者はそれを改善してその型パラメーターを取り除くことができます!

5
bluss

gcctypeof extension に依存する別の例を次に示します。

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

これは、同一のオブジェクトを設定し、配列指定のイニシャライザで初期化することで機能します。配列が渡された場合、コンパイラーは満足しています。ポインターが渡された場合、コンパイラーは次のメッセージを表示します。

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

GNUと呼ばれる拡張機能 ステートメント式 を使用した解決策の1つを次に示します:

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

これは 静的アサーション を使用してsizeof(arr) != sizeof(void*)をアサートします。これには明らかな制限があります-たまたまちょうど1つのポインタ(たとえば、ポインタ/整数の1長の配列、または32ビットの4長のバイト配列)のサイズの配列では、このマクロを使用できませんプラットホーム)。しかし、それらの特定のインスタンスは十分に簡単に回避できます。

このソリューションは、このGNU拡張機能をサポートしていないプラットフォームには移植できません。そのような場合は、標準のマクロを使用することをお勧めします。誤ってマクロへのポインターを渡す心配はありません。

1
Adam Rosenfield

ひどい、はい、しかしそれは機能し、それは移植可能です。

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

これはコンパイル時に何も検出しませんが、stderrにエラーメッセージを出力し、それがポインターまたはの場合は-1を返します長さは1です。

==> [〜#〜] demo [〜#〜] <==

1
Michael M.

私の個人的なお気に入り、gcc 4.6.3と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);
}

コンパイラのプリント

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

コレクションのもう1つの例。

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

長所:

  1. 通常の配列、可変長配列、多次元配列、サイズがゼロの構造体の配列で動作します
  2. ポインタ、構造体、または共用体を渡すと、コンパイルエラー(警告ではない)が生成されます
  3. C11の機能に依存しない
  4. それはあなたに非常に読みやすいエラーを与えます

短所:

  1. それはいくつかのgcc拡張に依存します: TypeofStatement Exprs 、および(もしそれが好きなら) 条件付き
  2. C99 [〜#〜] vla [〜#〜] 機能に依存します
0
hurufu