ニッチな話題ですが、古い環境でもコンパイル時にテストしたいという話です。
こちらからの引用です。
上の記事にある通り、新しい規格のC言語とかgccとかC++では別の手段があります。
あと流儀?も大量にあります。
古い環境では結局どれを使えばいいかという話。
- C99,C89でもVisualStudioでも使える。
- 同じファイルで複数回使っても名前が被らないために行番を入れている。
- 構造体の中や共用体の中以外ならほとんどの場所で使える。
という工夫が入っているのが、こちらのCOMPILE_TIME_ASSERTとのこと。
#define STATIC_ASSERT(COND,MSG) typedef char static_assertion_##MSG[(!!(COND))*2-1]
#define COMPILE_TIME_ASSERT3(X,L) STATIC_ASSERT(X,static_assertion_at_line_##L)
#define COMPILE_TIME_ASSERT2(X,L) COMPILE_TIME_ASSERT3(X,L)
#define COMPILE_TIME_ASSERT(X) COMPILE_TIME_ASSERT2(X,__LINE__)
COMPILE_TIME_ASSERT(sizeof(long)==8);
int main()
{
COMPILE_TIME_ASSERT(sizeof(int)==4);
}
鑑賞
動作のコアはこれです。
char static_assertion[(!!(COND))*2-1]
- 配列のサイズが負になるとコンパイルできないという仕様を使っています。CONDがtrueのときサイズが1になり、falseのとき-1になります。
- 三項演算子を使った形
static_assertion_##MSG[(COND)?1:-1]
にしないのは古いcc65でも動作できるようにとのこと。さすがにどっちでもいい。
続いてこの範囲について考えます。
#define STATIC_ASSERT(COND,MSG) typedef char static_assertion_##MSG[(!!(COND))*2-1]
- typedef を使っているのが重要①。サイズが'!!(COND))*2-1'のchar型の配列 の別名
static_assertion_##MSG
を定義しています。あくまで別名の定義なので、配列の実体が作られることはありません。 - typedef を使っているのが重要②。.cのファイル内で別名を定義しているので、最大でも影響範囲はファイル内です。名前
static_assertion_##MSG
が重複するかどうかはファイル内だけ考えればいいことになります。 - トークン連結演算子 (##)により、MSGを変えることで宣言の重複を防ぎます。
最後にこの部分。
#define COMPILE_TIME_ASSERT3(X,L) STATIC_ASSERT(X,static_assertion_at_line_##L)
#define COMPILE_TIME_ASSERT2(X,L) COMPILE_TIME_ASSERT3(X,L)
#define COMPILE_TIME_ASSERT(X) COMPILE_TIME_ASSERT2(X,__LINE__)
- 自動的に重複を防ぐために行番号に変換される組み込みマクロ
__LINE__
を使います。STATIC_ASSERTでtypedefを使っているおかげで、考慮する範囲はファイル内のみになっていたのでした。よって__FILE__
の結合は不要です。 - トークン連結演算子 (##)や文字列化演算子(#) がある関数マクロの宣言ではマクロ定数は展開されません。一見無駄に見える
#define COMPILE_TIME_ASSERT2(X,L) COMPILE_TIME_ASSERT3(X,L)
の行は__LINE__
を数字に展開するためにあります。
ここまできてあれですが。
- 同じ内容の配列のtypedefが重複してもエラーにならないのでは?visual studioでちょっと実験した感じだとならないです。規格は調べきれず。多分、STATIC_ASSERT(COND,MSG)で十分だけど安全のためCOMPILE_TIME_ASSERT(X)を使います。
- 使ってないtypedefがあると警告がでるコンパイラがありました。LLVM (clang-cl)。抑制が要りそうです。