第10回 翻訳フェーズ

C言語のコンパイルは、大きく分けて、「前処理(プリプロセス)」、「(狭義の)コンパイル」、そして「リンク」の順に解決されることはよく知られています。それでは、それよりさらに踏み込んだ順序についてはどうでしょうか? 例えば、注釈(コメント)の除去とマクロの展開では、どちらが先に解決されるのでしょうか? このような順序は、JIS X3010:2003 の中の「5.1.1.2 翻訳フェーズ」で規定されています。まずは、翻訳フェーズがどうなっているのかを見てみましょう。

  1. ソースファイル中の物理的な文字を内部表現に置換する。改行コードや文字コードの置換もここで行われる。
    • 処理系定義の多バイト文字 → 対応するソース文字集合
    • 3 文字表記 → 対応する文字
  2. 行末に逆斜線文字 \ があれば、物理ソース行を連結し、論理ソース行を生成する。
  3. ソースファイルを前処理字句と空白類(注釈を含む)の並びに分解する。注釈はひとつの空白文字に置換する。
  4. 前処理指令(プリプロセッサ・ディレクティブ)を実行する。
    • マクロ呼出しを展開する。
    • _Pragma 式を実行する。
    • #include 指令を展開し、取り込んだソースファイルに対して、1.~4.を再帰的に実行する。
    • すべての前処理指令を削除する。
  5. 文字定数および文字列定数中の各要素を、対応する実行文字集合に置換する。
  6. 隣接する文字列リテラルを連結する。
  7. 翻訳単位として翻訳する。
    • 前処理字句 → 字句に変換する。
    • 字句の構文および意味を解析する。
  8. すべての外部オブジェクトおよび外部関数の参照を解決する。

大まかな流れは上記のようになります。ここで、1.~6.がいわゆる「前処理」、7.が「(狭義の)コンパイル」、そして8.がリンクに相当します。

翻訳フェーズが分かれば、マクロの展開と注釈(コメント)の除去はどちらが先に行われるのかも分かるはずです。また、// で始まる行の末尾に逆斜線文字 \ があれば、次の行までコメントアウトされてしまう理由も納得できるはずです。

ここで一点注目して欲しいのは、狭義のコンパイルの際に解析の対象となる「字句(トークン)」は、ソースファイルからいきなり取り出されるのではなく、いったん「前処理字句」に分解されたあとで字句に変換されるという点です。「前処理字句」というのは、「ヘッダ名」、「識別子」、「前処理数」、「文字定数」、「文字列リテラル」、「区切り子」、およびその他の空白類以外の文字のことです。そして、この中で特に厄介なのが「前処理数」です。

「前処理数」というのは、文字通り数値定数のための前処理字句ですが、#if 指令の条件式で使う数値定数とも違います。ごく簡単にいえば、整数か浮動小数点数かを区別するまえの大雑把な解析によって得られた字句のことです。そして、この前処理字句を更に解析して、「整数定数」や「浮動小数点定数」に変換されることになるのです。前処理数の構文は次のようになります。

pp-number:
           digit
           . digit
           pp-number digit
           pp-number identifier-nondigit
           pp-number e sign
           pp-number E sign
           pp-number p sign
           pp-number P sign

つまり、この構文に合致するものは、すべて前処理数ということになります。例えば、0x1e- なども前処理数なのです。すなわち、

unsigned int a = 0x1e-0x10;

のようなコードがあった場合、一見すると、a0x1e から 0x10 を引いた値 0x0e で初期化しているようですが、これはコンパイルエラーになります(ただし、標準準拠度が低いコンパイラの場合、コンパイルできてしまうことがあります)。0x1e-0x10 から数値はに変換できないからです。もっと具体的にいえば、翻訳フェーズ 3. で、0x1e-0x10 はひとつの前処理数と解釈され、その後、翻訳フェーズ 7. で字句に変換しようとしたとき、0x1e-0x10 の変換に失敗してエラーになるのです。