sizeof演算子のように、配列型のオブジェクトを実引数として渡すと、要素数を表す定数式に展開してくれるマクロが欲しくなるときはよくあります。そんなときは、次のような定番のマクロを定義することで対応するのが普通だと思います。
#define countof(array) (sizeof(array)/sizeof((array)[0]))
これでも、一応最低限の要件は満たすわけですが、次のような使い方をした場合には、期待通りの振る舞いになりません。
int a[10];
int *p = a;
size_t n = countof(p);
これは、使い方が間違っているので正しく機能しないのは当然なのですが、それならそれで、コンパイルエラーになって欲しいところです。そこで、もう少し工夫してみることにしましょう。
まず、countofマクロの実引数が配列型かポインタ型かを見分ける方法を検討してみることにします。配列型というのは、ほとんどの演算で暗黙的にポインタ型に型変換されてしまいます。僅かな例外がsizeof演算子と&演算子です。
このうちsizeof演算子では、配列型のサイズとポインタのサイズがたまたま同じになることも十分考えられるので、あまり有効な手段とはいえません。では、&演算子はどうでしょうか?
&演算子を配列型に適用した場合、そこで得られる配列型へのポインタが指すアドレスと、配列型が暗黙的にポインタ型に変換されたときのアドレスは同じになるはずです。つまり、
int a[10];
(void*)&a == (void*)a; /* 真のはず */
それに対して、ポインタ型に&演算子を適用した場合には、両者が指すアドレスは異なるはずです。また、ポインタ型の場合は右辺値の可能性もあり、その場合に&演算子を適用しようとするとコンパイルエラーを生じます。
これらの振る舞いは、規格上は必ずしも保証されるわけではないでしょうが、現実的にはそのように考えて問題ないはずです。ならば、これらの性質を利用することで、配列型以外を渡した場合にエラーになるcountofマクロを定義できるはずです。
エラーにできるといっても、実行時エラーが発生するのはあまり旨みがありませんし、第一定数式に展開されるという要件を満たせなくなってしまいます。したがって、エラーの検出は、コンパイル時に行える(すなわちコンパイルエラーを発生させる)ものでなければなりません。
そこで最初に考えたのは、次のようなチェックコードを埋め込む方法です。
#define check_array(array) sizeof(int[(array)==(const volatile void*)&(array)])
#define countof(array) (check_array(array), sizeof(array)/sizeof((array)[0]))
ここで現れるcheck_arrayマクロでは、要素数が (array)==(const volatile void*)&(array) 個の int 型の配列のバイト数を取得しようとしています。sizeofを使っているのは、オペランドの評価を防ぐためです。
この方法では、arrayが配列型の場合には (array)==(const volatile void*)&(array) が定数式になりますが、それ以外の場合は定数式にならないか、arrayが右辺値であるために&演算子が使えないとしてエラーが発生します。
この方法も悪くないのですが、ひとつ重大な欠陥があります。というのは、C99やGCCでは可変長引数が使えるため、配列の要素数は必ずしも定数式でなくてもかまいません。そのため、arrayがポインタ型でもコンパイルエラーにならないのです。もう一ひねりして、
#define check_array(array) sizeof(int[(array)==(const volatile void*)&(array)]?1:-1)
のようにしたとしても、要素数は実行時まで評価されないため、やはりコンパイルエラーにすることはできません。
仕方がないので、配列の要素数を使うことは諦め、定数式が要求される別のシンタックスを使うことにしましょう。定数式が必要になるのは、他には caseラベル、構造体や配列の初期化子、列挙子があります。しかし、caseラベルを式中で書くのは流石に無理です。初期化子については、C99で導入された複合リテラルを使えば式中で記述できますが、定数式以外を初期化子に使えるような拡張も多く、第一C99以外では複合リテラルが使えません。
そこで、最後に残った手段が列挙子ということになります。列挙子を使って check_arrayマクロを書き直してみましょう。
#define check_array(array) sizeof(enum { argument_is_not_an_array = ((array)==(const volatile void*)&(array)) })
列挙子の名前を argument_is_not_an_array としたのは、コンパイルエラーが発生したときに、少しでもその原因を理解しやすくするための配慮です。これで、arrayが配列型の場合は (array)==(const volatile void*)&(array) が定数式になるので、列挙子の定義が正しく行われ、そうでなければコンパイルエラーが発生するようになりました。
これで一件落着かというとそうではありません。なぜなら、同じ有効範囲の中で、countof(というかcheck_array)マクロを複数回使うと、argument_is_not_an_arrayが重複定義された旨のエラーが生じるからです。
そして、この複数回使用問題の対策まで行ったものは、次のようになります。
#define check_array(array, line) check_array_(array, line)
#define check_array_(array, line) sizeof(enum { argument_is_not_an_array_##line = ((array)==(const volatile void*)&(array)) })
#define countof(array) (check_array(array, __LINE__), sizeof(array)/sizeof*(array))
最初に比べると随分複雑になってしまいましたが、このマクロを使う側からすれば、使い方は従来どおりですので、まあ良しとしましょう。それと、インクルードされたファイルでは、行番号がたまたま同じになってしまう可能性もあるわけですが、運悪くその問題に直面した場合は、空行を入れるなどして回避してください。
移植性という観点からは、実はもう一点問題が残っています。それは、__LINE__マクロというのは整数定数に展開されなければならないのですが、某有名コンパイラの場合、/ZIオプションを付けると汎整数定数式に展開されてしまい、結果として、argument_is_not_an_array_##line の部分がうまくいきません。コンパイラが規格の要求を満たしていないので、知らないと突っぱねてもよいのですが、どうしても対応したい場合には、そのコンパイラのときだけ、__LINE__の代わりに__COUNT__を使うなどすればよいかと思います。
定数式じゃない。(GCCのバグでエラーにならないだけ)
ご指摘の通り、定数式ではないですね。
(array)も(const volatile void*)&(array)も定数式ですが、整数定数式のオペランドにアドレス定数が使えないところに引っかかっています。
(array)と&(array)がともに定数式であることがポインタと配列を静的に区別するための唯一の方法だと思うので、もう少しよい方法を考えてみたいと思います。
最悪、規格厳密合致でなくても、主要な処理系だけでも使える方法なら、それはそれで有益ですから。
ところで、(array)==(const volatile void*)&(array)がエラーにならないのはGCCのバグというわけではないと思います。
6.6 定数式の最後から2行目に、
「処理系は, 定数式の他の形式を許してもよい。」
という記述があるからです。
(array)と&(array)がともに定数式 ==> arrayは配列型 というのは真だと思うけど,
逆は必ずしも真ではないし、あまり凝らない方がいいと思う。
C++なら方法もあるけど。
例1 arrayが定数式でない
int a[10][10];
int i;
countof(a[i])
例2 &演算子が適用できない
int (*a())[10];
countof(*a())
確かにそうですね。
あれから少し考えてみたけれども、一般的な方法はどうもなさそうです。
まあ、「方法はない」と結論だけを示すのは簡単ですが、さんざん検討や議論を行った過程を残しておくのは有益だと思います。
> C++なら方法もあるけど。
そうなんですよ。
C++なら、この辺のことをやるのは(簡単とはいいませんが)いくらでも方法がありますね。