今回はかなり難しい話です。まずは、次のコードをご覧ください。
0 1 2 3 4 5 6 7 8 9 10 11 |
#include<stdio.h> int main(void) { if (0xe-0xe) puts("A"); else puts("B"); return 0; } |
このプログラムをコンパイルし実行すると、出力されるのは”A”でしょうか? それとも”B”でしょうか? 実際に試してみることなく、どんな振る舞いになるかがわかった方は、C言語に相当詳しい方です。
答えは”A”でもなければ”B”でもありません。処理系の不具合、または独自拡張がない限り、このソースコードはコンパイルできません。実際にGCC 11.1で試してみると、
0 1 2 3 4 |
エラー: 整数定数に無効な接尾辞 "-0xe" があります 4 | if (0xe-0xe) | ^~~~~~~ |
というエラーメッセージが出力されました。理由が分からないと、このエラーメッセージを見ても、何のことかさっぱり分かりませんね。
それでは順を追って説明することにしましょう。いわゆる「リンク」に相当する部分を除けば、C言語もC++も次の翻訳フェーズを経てコンパイルされます。
- 物理文字からソース文字集合への変換
- 物理行から論理行への変換
- 前処理字句と空白類文字の分解
- 前処理指令の実行
- ソース文字集合から実行文字集合への変換
- 文字列リテラルの連結
- 字句の並びに対する解析
このうち1.~6.は、いわゆる「前処理(preprocess)」に相当する部分であり、6.を終えた時点でひとつの翻訳単位が完成することになります。そして、7.がいわゆる狭義の「コンパイル」に相当する部分です。
ここで、3.に登場する「前処理字句(preprocessing token)」に注目してみましょう。C言語では、ソースファイルに記述されたコードは「字句(token)」に分解されます。しかし、字句への分解はいきなり行われるのではなく、より大雑把な字句である前処理字句に分解された後、必要な箇所で前処理字句から字句に変換されます。前処理字句には、識別子、前処理数、区切り子と演算子がありますが、今回の問題に関係するのは、このうちの前処理数です。
ここまで分かれば、後は前処理数の構文を調べるだけで何が起こったのかが見えてきます。
0 1 2 3 4 5 6 7 8 9 10 11 |
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 | pp-number '.' |
前処理数は、整数と浮動小数点数の区別も無ければ、10進数、8進数、16進数の区別もありません。数字で始まるか、小数点で始まり、直後に数字が続くかすれば、空白類、区切り子、演算子以外が続く限り、前処理数の一部とみなされてしまうのです。
最初に例として挙げたコードを再びご覧ください。数字である0から始まり、x, e, -, 0, x, eという前処理数の構成要素が続く 0xe-0xe は、翻訳過程7.で整数値に変換されることになります。しかし、0xeまではよいものの、その後の-0xeは添え字(例えばlong型ならLを付けるといったもの)としては不正ですので、コンパイルエラーになってしまうのです。
なお、一種の独自拡張として翻訳フェーズ7.の時点で再解釈することは禁止されていないので、問題なくコンパイルできてしまう処理系も実在します。