C言語の型の読み方

C言語の型の読み方の解説は探すと色々出てくるが、改めて自分で説明してみることにした。

用語説明

C言語のポインタ記法に代わる記法として、ここでは次のような記法を導入する。もちろんこれはC言語にはない。

  • T へのポインタを ptr<T> と書く。
  • Tn 個並んだ配列を arr<T, n> と書く。
    • 可変長配列を arr<T, *> と書く。
    • サイズ指定のない配列を arr<T,> と書く。
  • 関数を fun<int(int, int)> のように書く。
    • ノンプロトタイプ関数を fun<int(*)> のように書く。

変数名について

C言語の型は、構文上は変数名とセットで扱われる。対応する変数がない場所では、単に変数名部分が省略されたものとして扱われる。

前処理1: K&R関数定義の場合

関数定義 ({} が後続する) で以下の条件に当てはまるときは、次のような処理をする。

  • 引数部分が () の場合、ここに void を入れる。
  • 引数部分が (x, y) のように変数名のみの場合、その直後に int x, y; のような宣言があるのでこれを処理する必要がある。(詳しい説明は省略)

ステップ1: 宣言指定子と宣言初期化子に分離する

以下に挙げるものが宣言指定子 (declaration specifier)である。

  • 分類A: typedef extern static _Thread_local/thread_local auto register inline _Noreturn/noreturn _Alignas/alignas
  • 分類B: const restrict volatile _Atomic/atomic
  • 分類C: void char short int long float double signed unsigned _Bool/bool _Complex/complex struct union enum _Atomic()/atomic(), typedef名 (int32_t など)

これらを左から順に取り除いていって、残った部分 (変数名、ポインタ、配列、関数、初期化など) が宣言初期化子 (declarator-initializer)である。宣言初期化子はカンマ区切りで複数あるかもしれない。

例えば、

long const unsigned static long * volatile x[30] = {0}, (*y)[20];

の場合、以下のように分けられる。

  • 分類Aの宣言指定子: static のみ
  • 分類Bの宣言指定子: const のみ
  • 分類Cの宣言指定子: unsigned, long, long
  • 宣言初期化子その1 : * volatile x[30] = {0}
  • 宣言初期化子その2 : (*y)[20]

宣言を分割する

1つの宣言に、複数の宣言初期化子がカンマで区切られている場合は、宣言を分割する。例えば、上の例の場合、

long const unsigned static long * volatile x[30] = {0};
long const unsigned static long (*y)[20];

と分割する。

ステップ2: 宣言指定子を並び替える

宣言指定子は3種類に分けられるので、分類する。

static const unsigned long long * volatile x[30] = {0};
static const unsigned long long (*y)[20];

ここで、「unsigned long long」という3つのキーワード(順不同だが重複は数える)の並びが、1つの型をあらわしている。わかりやすさのために、以降はアンダースコアで結ぶことにする。

static const unsigned_long_long * volatile x[30] = {0};
static const unsigned_long_long (*y)[20];

ステップ3: 宣言子を分解する

宣言子を以下のルールで分解する。

  • T * q dq ptr<T> d
  • T d[q n]q arr<T, n> d (nは特別な記号 * の場合や、何も書かれていない場合もある)
  • T d(A x, B y)fun<T (A x, B y)> d など
    • 例外1: T d()fun<T (*)> d
    • 例外2: T d(void)fun<T ()> d
  • T (d)T d

ただし、

分類Aの宣言指定子と初期化子は変換に巻き込まない。複数のルールが適用可能なら上のほうのルールを優先する。

上記のルールは適用できなくなるまで繰り返す。宣言子についている皮を剥いでいって、剥いだ順に宣言指定子側に被せていく感じになる。つまり順番が逆になる。

例えば以下のように変形する。

static const unsigned_long_long * volatile x[30] = {0};
static volatile ptr<const unsigned_long_long> x[30] = {0};
static arr<ptr<const unsigned_long_long>, 30> x = {0};

static const unsigned_long_long (*y)[20];
static arr<const unsigned_long_long, 20> (*y);
static arr<const unsigned_long_long, 20> *y;
static ptr<arr<const unsigned_long_long, 20>> y;

ステップ4: 関数の引数を処理する

もし関数の型が含まれていたら、その引数もまた宣言である。そこでこの処理を再帰的に繰り返す。

関数の引数の変数名は無視できるので、削除する。

ステップ5: 配列型を変換する

関数の引数部分については、配列をポインタに降格させる。例えば、

fun<int (int, arr<arr<int, 20>, 10>)>

のような型の場合は、 arr<arr<int, 20>, 10> が関数の引数なのでこれをポインタに降格させて、

fun<int (int, ptr<arr<int, 20>>)>

とおく。この処理は上のように、配列が関数の引数の内部に間接的に出現する場合は関係ない。

※この処理は、配列がtypedefされた型に対しても行う。

また、関数の引数に間接的に出てくる配列の大きさが定数ではない場合は、これを * に置き換える。

ステップ6: 完成

おわり

まとめ

C言語の型の構文は、意味を素直に表現していないので混乱のもとである。しかし、「宣言子から皮を剥がしていく作業」の影響範囲をきちんと理解すれば特に怖いものではない。