とくにあぶなくないRiSKのブログ

危ないRiSKのブログだったかもしれない。本当はRiSKだけどググラビリティとか取得できるIDの都合でsscriskも使ったり。

_Static_assert とは

C言語の次期規格 C1X で追加される _Static_assert キーワード。こいつは一体何者で,またどのように使うのでしょうか?

制約のチェック

プログラムの正しさを確認するためにチェックコードを入れることがあります。まずどのようにチェックするか,またどの時点でチェックするのか考えてみたいと思います。その流れで _Static_assert の必要性を見つけてみましょう。

assert による実行時のチェック

次のコードを見てみましょう。

#include<assert.h>
#include<stdio.h>

// 文字列 s を出力する。改行を付け足すことはしない。
// 事前条件:s は NULL ではない
static void put_string(const char* const s){
 assert(s);
 printf("%s", s);
}

int main(){
 put_string("hello, world"); // OK.
 put_string(NULL); // NG!! Bad call...
}

ここで put_string(NULL); は事前条件を満たしておらず,呼出しに問題があります。プログラムを実行し,実際にその呼出しが行われた時点で assert に引っ掛かり,プログラムは診断メッセージを出し止まります。プログラマはプログラムが止まってくれたことでバグの存在に気づけます。

#if と #error によるプリプロセス時のチェック

続いて次のコードを見てみましょう。

#if !defined(_MSC_VER) // _MSC_VER はコンパイラが VC の時に定義される
#error This program supports only Microsoft Visual C++.
#endif

int main(){
 // VC 固有のプログラム
}

このコードを gcc など VC 以外のコンパイラでコンパイルすると,プリプロセス時にエラーメッセージが表示され,コンパイルに失敗します。プログラマプリプロセスで止まってくれたことで間違った方法でコンパイルをしたことに気づけます。

コンパイル時のチェック

では,(プリプロセスではない)コンパイル時にエラーを見つけコンパイルを中止させることはできるのでしょうか?
C99 自体には直接そうできる機能はありません。しかしハックコード(?)を用いれば可能になります。次のコードを見てください。

#include<stdio.h>

#define STATIC_ASSERT(constexpr) \
 do{char dummy[(constexpr) ? 1 : -1];dummy;}while(0)

struct S{
 int a;
 char b;
};

int main(){
 STATIC_ASSERT(sizeof(struct S) == 5);

 FILE* const f = fopen("data.file", "wb");
 const struct S data = {1234, 'z'};
 if(!f)return 1;
 fwrite(&data, sizeof(data), 1, f);
 fclose(f);
 // 5 バイトの data.file ができたはず。
}

セーブデータを読み込んだり書き込んだりするにはセーブデータのサイズなどを揃える必要があります。上のコードの場合,セーブデータを表す構造体のサイズを5とはっきり決めています*1。もし構造体のサイズが8など5でない値ならばコンパイルエラーがおきます。これにより,間違ったセーブデータの読み書きが実行時に起こることを未然に防止するわけです。コンパイル時にエラーが起きたことで,プログラマ処理系依存の #pragma などを用いて構造体のサイズが5になるよう修正する機会をえます。

問題点

ここまで実行時,プリプロセス時,コンパイル時に制約をチェックする方法を見てきました。どれも有効な機能やテクニックですが,唯一コンパイル時のチェックだけがテクニックによる裏技的なものになっていました。
この弊害として,適切なエラーメッセージを表示できないという問題も生じます。実際,表示されるエラーは「添字が負の数です」のような見当違いなものです。

_Static_assert の役割

そこで,裏技を使うことなく分かりやすいメッセージを表示できるようにするために現れたのが,C1X の _Static_assert です。
_Static_assert はコンパイル時の assert を実現します。また,プログラマが分かりやすいエラーメッセージを指定できます。

使い方

先程の STATIC_ASSERT マクロを使っていたソースを _Static_assert を使うように書きかえてみましょう。

#include<stdio.h>

struct S{
 int a;
 char b;
};

int main(){
 _Static_assert(sizeof(struct S) == 5,
  "The size of S should be 5 bytes for the \"data.file\" file.");

 FILE* const f = fopen("data.file", "wb");
 const struct S data = {1234, 'z'};
 if(!f)return 1;
 fwrite(&data, sizeof(data), 1, f);
 fclose(f);
 // 5 バイトの data.file ができたはず。
}

このように使います。sizeof(struct S) == 5 ではない時に,STATIC_ASSERT マクロを使っていたコードより分かりやすいエラーメッセージが表示されることを期待できます。

スマートな static_assert

「"_Static_assert" はダサい」と思っている方も多いと思います。また,「C++0x には static_assert が入るのに,C1X は名前違うの?」と思っている方も多いと思います。
そんな皆様に朗報。 を #include すれば _Static_assert ではなく static_assert を使うことができます。
static_assert は の中で _Static_assert に展開されるマクロとして定義されています。

#include<assert.h>
int main(){
 static_assert(sizeof(int) == 4, "The size of int should be 4 bytes.");
}

こんな感じで使えます。この使い方は C99 で入った _Bool と似ていますね*2
以下,直訳っぽいお固い説明。

構文規則

static_assert宣言:
_Static_assert ( 定数式 , 文字列リテラル ) ;

制約

定数式は0と比較して等しくないこと。

意味規則

定数式は整数定数式でなければならない。もし定数式の値が0と等しくない場合,この宣言は何もしない。そうでない場合,制約違反であり,処理系はメッセージに現れることを要求されていないソース基本文字集合に属さない文字を除く文字列リテラルのテキストを含む診断メッセージを提供する。

原文

Otherwise,the
constraint is violated and the implementation shall produce a diagnostic message that
includes the text of the string literal, except that characters not in the basic source
character set are not required to appear in the message.

twitter にてヘルプを頂きましたが,私の英語力に自信がないため原文も貼付け…。
Twitter / ssc RiSK: 「例外としてこのメッセージには基本文字集合に含まれな ...
Twitter / ssc RiSK: 翻訳したい原文「except that charac ...
Twitter / 熱血プログラマー: @sscrisk 「メッセージに現れることを要求され ...

関係する構文規則まとめ

declaration:
declaration-specifiers init-declarator-listopt ;
static_assert-declaration
struct-declaration:
specifier-qualifier-list struct-declarator-listopt ;
static_assert-declaration
static_assert-declaration:
_Static_assert ( constant-expression , string-literal ) ;

構文を見るに _Static_assert は宣言という扱いのようです。変数宣言できるところと構造体の宣言の内部で使うことができるみたいです。構造体の宣言内部で使えるところも STATIC_ASSERT マクロより優れていますね。

参照元

Draft N1425 (PDF) 6.7.10 Static assertions, 7.2 Diagnostics

*1:このコードではパディングやアラインメントを無視しています

*2:<stdbool.h> を #include すれば _Bool ではなく bool (と true, false)を使うことができます