C P P - T E S T . T X T /* * Document of * "Validation Suite for Standard C Conformance of Preprocessing" */ 松井 潔 kmatsui@t3.rim.or.jp V.1.0 1998/08 First released. kmatsui V.1.1 1998/09 Updated according to C99 1998/08 draft. kmatsui V.1.2 1998/11 Updated according to C++98 Standard. kmatsui V.1.3 prerelease 1 2002/08 Updated according to C99 Standard. kmatsui V.1.3 prerelease 2 2002/12 Slightly modified. kmatsui V.1.3 release 2003/02 Finally released. Added porting to GNU C / testsuite. kmatsui V.1.3 patch 1 2003/03 Made the testsuite edition applicable to GNU C as old as 2.9x. kmatsui V.1.4 prerelease 2003/11 Added Visual C++ evaluation. kmatsui V.1.4 release 2004/02 Added tests of various multi-byte character encodings. kmatsui V.1.4.1 2004/03 Revised the section 2.4.26 (on recursive macro) kmatsui V.1.5 2005/03 Moved tests of multi-byte character encoding to quality matters. Changed point allocation of the test items. Added a few testcases for macro expansion. Updated a few testsuite testcases to cope with GNU C 3.3 and 3.4. Removed test results on older preprocessors (DJGPP, compiler systems on MS-DOS except Borland C 4.0). kmatsui 0.Standard C と Validation Suite 1.Standard C プリプロセスの特徴 [1.1] K&R 1st. と Standard C のプリプロセス [1.2] Translation phases [1.2.1] 行接続は tokenization の前に [1.3] Preprocessing token [1.3.1] Keyword がない [1.3.2] Preprocessing-number [1.3.3] Token-base の動作と token の連結 [1.4] #if 式の評価の型 [1.5] Portable なプリプロセッサ [1.6] 関数様マクロの展開方法 [1.6.1] 関数呼び出しと同等 [1.6.2] 引数は置き換えの前に展開される [1.6.2.1] #, ## 演算子の operand は展開されない [1.6.3] 再走査とは [1.6.4] 同一マクロの再帰的展開の防止 [1.7] 問題点 [1.7.1] の形の header name [1.7.2] Character-base のなごりを残す # 演算子の規定 [1.7.3] マクロ再定義時の white spaces の扱い [1.7.4] 関数様マクロ再定義時のパラメータ名 [1.7.5] 何を評価するのかわからない #if 式の文字定数 [1.7.6] マクロ再走査の関数様でない規定 [1.7.7] C90 Corrigendum 1, Amendment 1 での追加 [1.7.8] 冗長な規定 [1.8] C99 のプリプロセス規定 [1.9] 明快なプリプロセス規定を 2.Validation Suite 解説 [2.1] Validation Suite for Conformance of Preprocessing [2.2] テスト方法 [2.2.1] 手動テスト [2.2.2] cpp_test による自動テスト [2.2.3] GNU C / testsuite による自動テスト [2.2.3.1] TestSuite とは [2.2.3.2] TestSuite へのインストールとテスト [2.2.3.3] MCPP の自動テスト [2.2.3.4] TestSuite と検証セット [2.3] Violation of syntax rule or constraint と診断メッセージ [2.4] 詳細 [2.4.1] Trigraphs [2.4.2] による行接続 [2.4.3] コメント [2.4.4] 特殊なトークン (digraphs) と文字 (UCN) [2.4.5] Preprocessing directive line 中の space, tab [2.4.6] #include [2.4.7] #line [2.4.8] #error [2.4.9] #pragma, _Pragma() operator [2.4.10] #if, #elif, #else, #endif [2.4.11] #if defined [2.4.12] #if 式の型 [2.4.13] #if 式の演算 [2.4.14] #if 式のエラー [2.4.15] #ifdef, #ifndef [2.4.16] #else, #endif のエラー [2.4.17] #if, #elif, #else, #endif の対応関係のエラー [2.4.18] #define [2.4.19] マクロの再定義 [2.4.20] Keyword と同名のマクロ [2.4.21] Pp-token の分離を要するマクロ展開 [2.4.22] Pp-number 中のマクロ類似 sequence [2.4.23] ## 演算子を使ったマクロ [2.4.24] # 演算子を使ったマクロ [2.4.25] マクロ引数中のマクロの展開 [2.4.26] マクロ再走査中の同名マクロ [2.4.27] マクロの再走査 [2.4.28] 事前定義マクロ [2.4.29] #undef [2.4.30] マクロ呼び出し [2.4.31] マクロ呼び出しのエラー [2.4.32] #if 式の文字定数 [2.4.33] #if 式のワイド文字定数 [2.4.35] #if 式の multi-character 文字定数 [2.4.37] Translation limits [2.5] 処理系定義部分のドキュメント 3.規定されていない諸側面の評価 [3.1] Multi-byte character encoding [3.2] Undefined behavior [3.3] Unspecified behavior [3.4] ウォーニングの望ましいその他のケース [3.5] その他の各種品質 [3.5.1] 動作に関する品質 [3.5.2] オプションと拡張機能 [3.5.3] 実行効率等 [3.5.4] ドキュメントの品質 [3.6] C++ のプリプロセス 4.Cプリプロセスの周辺 [4.1] 標準ヘッダファイル [4.1.1] 一般的規約 [4.1.2] [4.1.3] [4.1.4] 5.各種プリプロセッサのテスト結果 [5.1] テストしたプリプロセッサ [5.2] 採点表 [5.3] 各プリプロセッサの特徴 [5.4] 総評 [5.5] テスト報告とご意見を ☆ 0.Standard C と Validation Suite ☆ 私は Martin Minow 作の DECUS cpp を全面的に書き直して MCPP V.2 と称す る portable なCプリプロセッサを作りました。このプリプロセッサはソースで 提供するもので、コンパイルする時にヘッダファイル中のいくつかのマクロを書 き替えることで各種の処理系用に移植できるようになっており、また他のマクロ を書き替えることで Standard C (ISO/ANSI/JIS C) を初めとする各種の動作仕 様の実行プログラムが生成されるようになっています。それらの仕様のうちの Standard C モードは、文字通りの厳密な Standard C プリプロセスを実現して いるつもりです。*1 このプリプロセッサを作りながら私は、そのテストのために "Validation Suite for Standard C Conformance of Preprocessing" (プリプロセスの標準 C適合性を検証するソフトウェア一式)と称するものを作りました。このドキュ メントはその検証セット (Validation Suite) の解説です。この検証セットはこ のドキュメントとともに free software として公開します。 この検証セットは 1998/08 に NIFTY SERVE / FC / LIB 2 で公開され、http: //www.vector.co.jp/pack にも転載されました。これには version number があ りませんでしたが、これを version 1.0 であったことにします。 V.1.1 は、C99 1997/08 draft に対応して V.1.0 を update したものです。 1998/09 に、やはり NIFTY SERVE / FC / LIB 2 および vector / software pack で公開されました。 V.1.2 は正式に決定した C++ Standard に対応して、V.1.1 にささいな updates を加えたものです。1998/11 に、やはり NIFTY SERVE / FC / LIB 2 お よび vector / software pack で公開されました。 V.1.3 は正式に決定した C99 に対応して、V.1.2 に updates を加えたもので す。また、動作テストのサンプルは、GNU C / testsuite で利用できるように書 き直した edition が追加されました。 V.1.3 は開発の途中で、MCPP V.2.3 とともに、情報処理振興事業協会 (IPA) の平成14年度「未踏ソフトウェア創造事業」に新部 裕・プロジェクトマネー ジャによって採択され、2002/07 - 2003/02 の間は IPA の資金援助と新部PM の助言のもとに開発が進められました。英語版ドキュメントもこのプロジェクト の中で、有限会社・ハイウェルに翻訳を委託し、それに私が修正とテキスト整形 を加えてできあがったものです。2003/02 には MCPP V.2.3 と検証セット V.1.3 が m17n.org で公開されました。 さらに MCPP と検証セットは平成15年度「未踏ソフトウェア創造事業」にも 伊知地 宏・PM によって継続して採択され、それぞれ V.2.4, V.1.4 への update 作業が進められました。*2 その後も MCPP と検証セットはさらに改良の作業が続けられています。2005/ 03 にはそれぞれ V.2.5, V.1.5 がリリースされました。検証セット V.1.5 では 配点の変更などがありました。 C言語の規格としては従来 ISO/IEC 9899:1990 (JIS X 3010-1993) が使われ てきましたが、1999 年には ISO/IEC 9899:1999 が採択されました。ここでは前 者を C90、後者を C99 と呼びます。前者は ANSI X3.159-1989 が移行したもの なので、一般には ANSI C または C89 と呼ばれることもあります。また、ISO/ IEC 9899:1990 + Amendment 1995 を C95 と呼ぶことがあります。 この解説で参照する「規格書」は次のものです。 C90: ANSI X3.159-1989 (ANSI, New York, 1989) ISO/IEC 9899:1990(E) (ISO/IEC, Switzerland, 1990) ibid. Technical Corrigendum 1 (ibid., 1994) ibid. Amendment 1: C Integrity (ibid., 1995) ibid. Technical Corrigendum 2 (ibid., 1996) JIS X 3010-1993 (日本規格協会「JIS ハンドブック 59-1994」、東京、1994) C99: ISO/IEC 9899:1999(E) ibid. Technical Corrigendum 1 (2001) ibid. Technical Corrigendum 2 (2004) C++: ISO/IEC 14882:1998(E) ANSI X3.159 には "Rationale"(理由書)が付属していました。これは ISO C90 にはなぜか採用されませんでしたが、ISO C99 では復活しました。この "Rationale" も折りに触れて参照します。 C99, C++ の規格書は PDF でフォーマットされたオンライン版が次のところで 入手できます。 C99, C++98 http://webstore.ansi.org/ansidocstore/default.asp C99 Corrigendum 1 http://ftp2.ansi.org/download/ free_download.asp?document=ISO%2FIEC+9899%2FCor1%3A2001 C99 Rationale 1999/10 final draft http://anubis.dkuug.dk/JTC1/SC22/WG14/www/docs/n897.pdf *1 この cpp は V.2.2 までは単に cpp と呼んでいたが、一般の cpp と紛ら わしいので、 V.2.3 からは MCPP と呼ぶことにした。Matsui CPP の意味で ある。このドキュメントでは V.2.2 までのバージョンも MCPP と呼ぶ。ま た、このドキュメントの名前は V.1.2 までは cpp_test.doc としていたが、 V.1.3 からは cpp-test.txt と変更した。私自身の名前も、V.1.2 までは Psycho としていたが、V.1.3 からは kmatsui と変更した。 *2 「未踏ソフトウェア創造事業」(Exploratory Software Project) の概要 は次のところで知ることができる。 http://www.ipa.go.jp/jinzai/esp/ なお、情報処理振興事業協会 (IPA) は 2004/01 に独立行政法人・情報処理 推進機構 (IPA) に改組された。 MCPP V.2.3 以降のソースおよびドキュメントと検証セット V.1.3 以降は最 新バージョンを含めて、次の CVS repository に置いている。ここから tar- ball を download できる。 http://cvs.m17n.org/cgi-bin/viewcvs/?cvsroot=matsui-cpp 次のところからは anonymous ftp できる。 ftp://ftp.m17n.org/pub/mcpp/ また、次のところには案内の web page がある。 http://www.m17n.org/mcpp/ cpp V.2.2 および検証セット V.1.2 はベクター社のサイトの次のところに ある。「PACK for WIN GOLD」という CD-ROM にも収録されていた。dos/ prog/c というディレクトリに入れられているが、MS-DOS 専用ではない。ソ ースは UNIX, WIN32/MS-DOS, OS-9 等に対応している。 http://download.vector.co.jp/pack/dos/prog/c/cpp22src.lzh http://download.vector.co.jp/pack/dos/prog/c/cpp22bin.lzh http://download.vector.co.jp/pack/dos/prog/c/cpp12tst.lzh http://download.vector.co.jp/ は ftp://ftp.vector.co.jp/ でも同じよ うである。 旧版は次のところにもある(ここは会員制の closed なフォーラムである)。 @nifty / FC / lib 2 これらのアーカイブファイル中のテキストファイルは、Vector のものは DOS/Windows 系に合わせて改行コードは [CR]+[LF]、漢字は shift-JIS で encode してある。m17n.org のものは UNIX 系に合わせて改行コードは [LF]、 漢字は EUC-JP である。他のOSで使う場合は変換が必要である。 拙作の convf というツールを使うと、全ファイルを一括して変換コピーで きるので簡単である(バイナリファイルは自動的に判別して無変換でコピー する。Time-stamp や mode は保存される)。ただし、MCPP のパッケージに は特定の multi-byte character encoding をテストするためのファイルも 含まれているので、それらは encoding を変換してはいけない。まず全ファ イルを改行コードだけ変換して一括コピーし、さらに doc ディレクトリだ け改行コードと漢字 encoding の双方を変換して上書きコピーするのが良い。 Convf そのものも MCPP を移植したのと同じ処理系に対応している。ただし、 ファイルを DOS/Windows 系から他のOSに持っていく場合は、MS-DOS や Windows95 上で解凍すると大文字と小文字の区別がなくなってしまうので、 アーカイブファイルのまま移してから、解凍して変換すること。Convf は次 のところにある。 http://download.vector.co.jp/pack/dos/util/text/conv/code/ convf-1.8.lzh ☆ 1.Standard C プリプロセスの特徴 ☆ Validation Suite の解説をする前に、Standard C (ANSI/ISO/JIS C) のプリ プロセスの全体的な特徴を説明しておきます。これは教科書的な説明ではなく、 K&R 1st. と比較しながら Standard C のプリプロセスの考え方と問題点を明ら かにしようとするものです。 説明の仕方としては、K&R 1st. と C90 との相違を中心とし、 C90 と C99, C++ との相違を付け加えるという順序をとります。現在は規格としては C99 が 有効なものですが、現実の処理系には C99 はまだあまり実装されていないので、 C90 を中心とするほうが実際的だと思われるからです。 なお、この章ではサンプルをまったく示していませんが、Validation Suite そのものがサンプルなので、そちらを参照してください。 [1.1] K&R 1st. と Standard C のプリプロセス Pre-Standard のC言語処理系には千差万別の方言がありましたが、中でもプ リプロセスにはほとんど基準がないと言ってよいくらいの状況でした。そうなっ た原因は、基準となるべき Kernighan & Ritchie "The C Programming Language", 1st. edition のプリプロセスの規定があまりにも簡単なあいまいなものであっ たことにあります。さらにその背景には、プリプロセスは言語本体に対する「お まけ」のようなものという考えがあったと思われます。しかし、K&R 1st. 以降、 各処理系によってプリプロセスには多くの機能が付け加えられてきました。その 内容には言語本体の不備を補うためのものと、異なる処理系間での portability を確保しようとするものとがありますが、どちらにしても処理系間の相違が多く、 portable であるには程遠いのが実情でした。 Standard C では長年の混乱の元であったこのプリプロセスに明確な規定が与 えられました。新しく追加された機能もいくつかあり、それらについてはよく知 られていますが、もっと大事なことは、Standard C がプリプロセスに関する事 実上初めての全面的な規定であるということです。この規定には、これまであい まいであった「プリプロセスとは何か」という基本的なことについての考え方が 随所に現れています。Standard C のプリプロセスは K&R 1st. + α ではないの です。これを理解するには「新機能」だけでなく、こうした基本を明確に把握す ることが必要だと思われます。しかし、残念なことには規格書本文にはそのこと がまとめて述べられてはおらず、規格書の注釈である "Rationale" でもサラリ と触れられているだけです。さらに残念なことには、旧来のプリプロセスとの妥 協の結果と思われる首尾一貫しない部分も残されています。そこで、以下に Standard C プリプロセスの基本的な特徴をまとめて述べ、次にその問題点を検 討することにします。 Pre-Standard のプリプロセスと異なる、あるいは初めて明確にされた Standard C プリプロセスの特徴は、次の4点にまとめることができます。 1.言語本体の処理系依存の部分(いわゆる実行時環境 execution environ- ment)からは独立した文字通りのプリプロセスである。処理系によってプリプロ セスが意外な結果になる恐れはきわめて少ない。またこれによって、プリプロセ ッサそのもののソースを portable に書くことが可能になった。さらに同一のO Sであれば、プリプロセッサの実行プログラムは1つでもすむと言って良いくら いである。。 2.Translation phases の規定によって、ソースを token に分割するまでの 手順が明確に定められている。Token もプリプロセスが終わるまでは pre- processing token という暫定的な形で扱われる。Preprocessing token という ものを本来の token と別に規定したことも、プリプロセスを言語本体の処理系 依存の部分から切り離すことに役立っている。 3.プリプロセスは preprocessing token を処理単位とする、原則として token-oriented なものである。これに対して pre-Standard のプリプロセスは token-oriented な建前も一方にありながら、歴史的経緯からくる character- oriented な処理をする部分を少なからず持つ中途半端なものであった。 4.関数様マクロの展開は関数呼び出しをモデルにすることで、文法が整理さ れている。関数様マクロの呼び出しは関数呼び出しの使えるところならどこでも 使える。引数中にマクロ呼び出しがある場合の処理も、関数の引数中に関数呼び 出しがある場合の評価とパラレルであり、引数中のマクロが完全に展開されてか ら置換リスト中のパラメータと置き換えられる。この時、引数中のマクロ呼び出 しはその引数内で完結していなければならない。 これらの原則を以下に順次、検討していきます。 [1.2] Translation phases プリプロセスの手順は K&R 1st. ではまったく記載されていなかったために、 多くの混乱の元となってきました。Standard C では translation phases とい うものが規定されて、これが明確にされました。要約すると次のようなものです。 *1 1.ソースファイルの文字を必要ならソース文字セットに map する。Tri- graph の置換をする。*2 2. を削除する。それによって物理行を接続して論理 行にする。 3.Preprocessing token と white spaces とに分解する。コメントは one space character に置換する。改行コードは保持する。 4.Preprocessing directive を実行し、マクロ呼び出しを展開する。# include directive があれば、指定されたファイルについて phase 1 から phase 4 を再帰的に処理する。 5.ソース文字セットから実行時文字セットへの変換をする。同様に、文字定 数中と文字列リテラル中の escape sequence を変換する。 6.隣接する文字列リテラルを連結し、隣接するワイド文字列リテラルを連結 する。 7.Preprocessing token を token に変換し、コンパイルする。 8.リンクする。 もちろん、これらは実際に別々の phase になっている必要はなく、それと同 じ結果になるように処理すれば良いことになっています。 このうち phase 1 から phase 4 または 6 までがプリプロセスの範囲に属し ますが、プリプロセッサが独立したプログラムになっていてプリプロセス結果を 中間ファイルとして出力する場合は、改行コード等の token separator を保存 する必要があるため、phase 4 までを担当するのが普通です(phase 5 で \n 等 の escape sequence を変換してしまうと、これが token separator としての改 行コード等と区別がつかなくなる)。この Validation Suite でテストするのも phase 4 までです。*3 *1 ANSI C 2.1.1.2 (C90 5.1.1.2) Translation phases 翻訳フェーズ C99 5.1.1.2 Translation phases C99 では phase 4 に _Pragma() operator の処理が付け加えられた。その ほか若干の字句が追加されているが、意味に変化はない。 *2 C99 1997/11 draft では、trigraphs の変換の前に、basic source character set に含まれない multi-byte character は \Uxxxxxxxx という 形の Unicode の16進 sequence すなわち universal character name に 変換する、という規定になっていた。そして、phase 5 でこれを実行時文字 セットに再変換するというのである。C++ Standard でもほぼ同様である。 仕様があいまいであるうえに、処理系にとっては負担の大きいことである。 しかし、幸いなことに 1999/01 draft で phase 1 の処理は削除され、C99 正規版でもその通りとなった。 *3 ただし、MCPP では CONCAT_STRINGS というマクロを TRUE と定義してコ ンパイルしておくと、phase 6 もプリプロセッサでやる。しかし、phase 5 をとばすと escape sequence を含む文字列リテラルが連結された時に間違 った結果になる恐れがあるので、phase 5 の代わりに文字列リテラルにちょ っとした手を加えている。実際の動作は misc.t / PART 6 参照。 また MCPP では、SJIS_IS_ESCAPE_FREE というマクロを FALSE と定義して コンパイルしておくと、shift-JIS 漢字を認識しないコンパイラ本体のため に、phase 4(または 6)が終わってから、文字列リテラルおよび文字定数 中の shift-JIS 漢字の2バイト目が 0x5c (\) であった場合はそこにもう 1つ 0x5c を挿入する。[2.4.36] 参照。 これらはいずれも、phase 5, 6 の処理が不十分なコンパイラ本体のための 機能である。こうした処理を早い phase でやってしまうと間違いが起こる が、phase 4 まで完全に終わってからであれば問題はない。 [1.2.1] 行接続は tokenization の前に による行接続は K&R 1st. では、 1.長い #define 行の途中にあるもの、 2.長い文字列リテラルの途中にあるもの、 の2つが記載されているだけで、その他の場合はどうなのかが明らかではありま せんでした。 Standard C では、phase 3 で preprocessing token と token separator と しての white spaces への分割が行われる前に、phase 2 で の削除が行われることが明確にされ、どんな行でも、どんな token の途中でも有効となりました。 また、trigraph の処理は phase 1 で行われるので、??/ という sequence も同様に削除されます。他方で、基本文字セットが ASCII で multi- byte character の encoding が shift-JIS 漢字である処理系では、漢字1字が 1つの multi-byte character であるので、漢字2バイト目の 0x5c のコードは ではありません。 Translation phase が明確になったのは良いのですが、はたして token の途 中での行接続などというものを認める必要があるのかどうかは、疑問です。K&R 1st. では画面1行におさまらない長い文字列リテラルを画面におさまるように 書くにはこの方法しかなかったわけですが、Standard C では隣接する文字列リ テラルが連結されることになったので、わざわざ token の途中で改行する必要 はなくなっています。行接続が必要なのは長いコントロール行を書く時だけです。 それだけであれば、phase 2 と 3 は逆のほうが良かったのです。 にもかかわらずこれが現在の規定のように決まったのは、K&R 1st. の文字列 リテラルの行接続による連結という仕様を前提として書かれたソースを処理でき るようにという、backward compatibility(後方互換性)のためだと思われます。 新しいソースについては実用上はほとんど意味のない規定ですが、しかし、単純 明快な、実装の最も容易なものであるので、妥当でしょう。 [1.3] Preprocessing token Preprocessing token(以後、pp-token と略す)という概念も Standard C で 初めて導入されたものです。しかし、これについては世間であまり知られていな いようなので、内容の要約から始めなければなりません。pp-token として規定 されているのは次の通りです。* header-name identifier preprocessing-number character-constant string-literal operator punctuator 上記のどれにも当てはまらない non-white-space character 何気なく見ると当たり前のようで見過ごしてしまいそうですが、これは本来の token とはかなり違っています。Token は次のようなものです。 keyword identifier constant (floating-constant, integer-constant, enumeration-constant, character-constant) string-literal operator punctuator Pp-token が token と違っているのは次の点です。 1.Keyword が存在しない。keyword と同じ名前は identifier として扱われ る。 2.Constant のうち character-constant は同じであるが、floating- constant, integer-constant, enumeration constant が存在せず、floating- constant, integer-constant の代わりに preprocessing-number というものが ある。 3.Header-name は pp-token としてしか存在しない。 4.Operator と punctuator はほぼ同じであるが、operator の #, ## と punctuator の # は pp-token としてしか存在しない(いずれも preprocessing directive 行でだけ有効)。 何と、同じなのは string-literal と character-constant だけなのです。中 でも重要なのは keyword が存在しないことと、数値 token に代わる pre- processing-number の存在です。この2点について、さらに検討します。 * ANSI C 3.1 (C90 6.1) Lexical elements 字句要素 C99 6.4 Lexical elements C99 では pp-token でも token でも operator は punctuator に吸収され た。Operator という用語は token の種類としてではなく、単に「演算子」 という機能を表す用語となった。同じ punctuator token (punctuator pp- token) が文脈によって punctuator (区切り子)として機能したり、 operator として機能したりすることになる。また、pp-token の演算子とし て _Pragma が追加された。 [1.3.1] Keyword がない Keyword は phase 7 で初めて認識されます。プリプロセスの phases では keyword は identifier として扱われます。そして、プリプロセスにとっては identifier はマクロ名であるか、マクロとして定義されていない identifier であるかのどちらかです (*1)。ということは、keyword と同名のマクロさえも 使えるということになります。 この規定は、プリプロセスを処理系依存部分から切り離すために不可欠のもの だと思われます。これによって例えば #if 式にキャストや sizeof を使うこと が禁止されます。*2 *1 もっと正確に言えば、マクロ定義中のパラメータ名も identifier である。 また、preprocessing directive 名は特殊な identifier であり、keyword と似た性格を持っている。しかし、これが directive であるかどうかは構 文から判断されるものであり、directive でない場所にあれば、マクロ展開 の対象ともなりうる単なる identifier である。 *2 [2.4.14.7], [2.4.14.8] 参照。 [1.3.2] Preprocessing-number Preprocessing-number(以後、pp-number と略す)は次のように規定されてい ます。*1, *2 digit . digit pp-number digit pp-number nondigit pp-number e sign pp-number E sign pp-number . Non-digit というのは letter と underscore です。 要約するとこうなります。 1.先頭が digit または . digit である。 2.あとは letter (alphabet), underscore, digit, period および e+, e-, E+, E- の sequence がいくつどういう順序で並んでいてもかまわない。 Pp-number は floating-constant, integer-constant のすべてを含みますが、 さらに例えば 3E+xy といった数値ではないものを幅広く含んでいます。Pp- number はプリプロセスを簡単にするために採用されたもので、意味解釈に先立 つこの種の sequence の tokenization に役立つとされています。*3 Tokenization が簡単になるのは確かですが、非数値 pp-number は有効な token ではありません。したがって、それらはプリプロセスの終わるまでに消滅 していなければなりません。非数値 pp-number というものをわざわざソース中 で使う必要はまずありませんが、あえて使うとすれば唯一考えられるのは、## 演算子を使って定義されたマクロで数値 pp-number と何かの pp-token とが連 結されて非数値 pp-number となり、さらに # 演算子を使って定義されたマクロ によってそれが文字列化される場合です。文字列リテラルの中に入れてしまえば、 どんな pp-token も valid な token になります。しかし、非数値 pp-number の存在を認めないと、連結によって生成されたものが valid な pp-token では なくなってしまいます(その結果は undefined となる)。*4 こういう使い道は極めて特殊なものであり詳細に検討する必要はありませんが、 ただ pp-number は token-oriented なプリプロセスを考えるうえで興味深い題 材を提供してくれます。 *1 ANSI C 3.1.8 (C90 6.1.8) Preprocessing numbers 前処理数 C99 6.4.8 Preprocessing numbers *2 C99 では浮動小数点数の16進表記を可能にするために、pp-number p sign, pp-number P sign という sequence が追加されている。 また、上記の nondigit が identifier-nondigit に置き換えられた。これ は identifier 中に UCN (universal character name) と処理系定義の multi-byte character の使用が認められたのに伴う変更である([1.8] 参 照)。すなわち、pp-number 中に UCN を使うことができ、multi-byte character を使う実装も可能である。数値トークンに UCN や multi-byte character が含まれることはないのであるが、やはり ##, # 演算子を使っ て文字列化する場合のことを考えて、これが認められているのである。 *3 C89 Rationale 3.1.8 Preprocessing numbers C99 Rationale 6.4.8 Preprocessing numbers *4 misc.t の PART 5 のサンプルを参照。 [1.3.3] Token-base の動作と token の連結 Standard C ではマクロ定義中の二項演算子 ## によって pp-token を連結す ることができるようになりました。このことは Standard C の「新機能」として よく知られています。しかし、これは追加された機能というよりは、旧来の裏技 的な方法を代替するために導入されたものです。注目したいのは、これが token- oriented なプリプロセスのために必須のものとなっていることです。 旧来の token 連結の方法としては、いわゆる "Reiser" 型 cpp での、コメン トを 0 個の character に置換するという仕様を利用したものが知られています。 また、それ以外にも character-oriented な動作をするプリプロセッサでは、意 図しない時にまで token の連結が発生してしまうことがあり、それを利用した 裏技的方法もないではありませんでした。それらはいずれも、character- oriented なプリプロセスの欠陥を利用する方法と言えます。 これに対して Standard C では、token-oriented な動作によって明示的に token を連結することができるようになっています。Translation phase 3 でソ ースが pp-token と white spaces の sequence に分解されますが、その後で pp-token が合成される場合というのは、## 演算子による連結と # 演算子によ る文字列化、header-name の生成、および隣接する文字列リテラルの連結とワイ ド文字列リテラルの連結しかありません。非数値 pp-number の存在もこの文脈 の中に置いて考えると、その扱い方が明確になります。すなわち、Standard C の tokenization については、次のような原則が存在していると言えるでしょう。 1.Pp-token が暗黙のうちに連結されることはない。連結は ## 演算子によ って明示しなければならない。 2.いったん連結された pp-token が再び分離されることはない。 Pre-Standard の character-oriented なプリプロセスでは、マクロ呼び出し が展開されると、その結果の token sequence が前後の token と意図しない連 結を引き起こすことがありました。しかし、これも token-oriented な Standard C のプリプロセスでは起こってはならないことだと考えられます。* * [2.4.21] 参照。 [1.4] #if 式の評価の型 C90 では #if 式の評価は long ないし unsigned long の1種類のサイズで行 われることになりました (*1)。これも、プリプロセスを単純化するとともに、 処理系依存部分を少なくするのに役立っています。int のサイズが処理系によっ て大きく変わるのに比べて、long / unsigned long は大半の処理系が32ビッ ト、一部の処理系だけが64ビットまたは36ビットですから、一般の #if 式 にはかなりの portability が確保できます。*2, *3 *1 ANSI C 3.8.1 (C90 6.8.1) Conditional inclusion 条件付き取り込み -- Semantics 意味規則 *2 C99 6.10.1 Conditional inclusion C99 では #if 式の型はその処理系の最大の整数型とされた。C99 では long long / unsigned long long は必須であるので、これは long long / unsigned longlong またはそれ以上ということになる。しかし、これによっ て #if 式の portability はかなり低下することになった。 *3 将来は long が64ビットの処理系が増えてくるかもしれないが、これは 良いことか悪いことか・・ 余談であるが、私は整数型のサイズは次のよう に決めるほうが良いと思っている。 1.short は2バイト、long は4バイトとする。 2.longlong(long long ではない)または quadra を8バイトとする。 3.int は CPU にとっての自然なサイズとする。すなわち、これは short, long, longlong のどれか1つと一致する。 4.short int, long int というふうに short, long を int の修飾に使う ことはやめる。 すなわち、64ビットの処理系の出現以来、sizeof (short) <= sizeof (int) <= sizeof (long) という制約があるためにすべてが不自然になり、 どの型も portability がなくなってきているのである。この制約をはずし て、絶対サイズで型を決めるのが良い。 [1.5] Portable なプリプロセッサ 以上のような Standard C プリプロセスの規定は、プリプロセッサそのものの ソースを portable に書くことを可能にしています。プリプロセッサがコンパイ ラ本体の処理系依存部分について知らなければならないことは、何もないからで す。実際に Standard C の処理系でプリプロセッサを portable に書こうとする 時に問題となるのは、次のような周辺的な部分だけです。 1.#include 処理のためのOSのパスリスト記述形式。標準ヘッダファイル のありか。 2.ファイル名と行番号の情報をコンパイラ本体に渡すための形式。 3.実行時オプション。 4.文字セット。 5.クロス処理系では、C90 では long / unsigned long の、C99 では最大の 整数型のサイズがターゲット処理系のそれを下回っていてはならないこと。 このうち2と3は既存の処理系に実装する場合に問題となるだけで、2もいず れはソースと同じ #line 123 "filename" という形式に統一されていくと期待さ れますし、3は無くても(使い勝手はともかく論理的には)Standard C のプリ プロセスは可能です。4もソースの書き方によっては、特別な implementation を必要としない形で書けなくはありません(しかし、基本文字セットはソース中 にテーブルを書くほうが実装が楽であるが)。5は実際には、ホスト処理系のほ うがターゲット処理系よりも整数型のサイズが小さいということはほとんどない でしょうから、問題とはならないでしょう。 MCPP ももちろん、こうした Standard C プリプロセスの仕様が処理系本体か ら独立していることを制作の動機としています(しかし、MCPP は pre-Standard の処理系でコンパイルすることも、pre-Standard の処理系に移植することも、 また pre-Standard や post-Standard の各種プリプロセッサを作ることも目的 にしているので、portability 確保のための #if section がかなり多くなって いるが)。 [1.6] 関数様マクロの展開方法 引数付きのマクロの展開方法は Standard C では関数呼び出しをモデルにして 規定され、function-like(関数様)マクロと呼ばれるようになりました。引数 にマクロが含まれていた場合は、原則として、それはパラメータとの置換に先だ って展開されます。 Pre-Standard ではこの点も明らかではありませんでした。実際には、引数中 のマクロは展開せずにパラメータと置換し、再走査時に展開するという方法をと るものが多かったのではないかと思われます。こういう展開方法の背景にあるの は、いわばエディタ様のテキスト置換の繰り返しという発想であったと推測され ます。引数なしマクロの展開は一般にエディタ様のテキスト置換の繰り返しでか まわないわけですが、それを引数付きマクロの展開にも広げたものが、多くのプ リプロセッサのマクロ展開方法だったのではないでしょうか。 しかし、この方法はソース上での関数呼び出し様の外観とはまったく異なった 奇妙なマクロの使い方を誘発し、ネストされた引数付きマクロの呼び出しの際に は、どれがどれの引数なのかわからなくなる事態も発生します。それらの点をめ ぐって処理系依存の部分も増えてきます。一言で言えば、Cのマクロ展開は高度 な機能を担うようになったために、エディタ様のテキスト置換の繰り返しという 発想ではもはや荷が重くなりすぎたと言えるでしょう。 [1.6.1] 関数呼び出しと同等 こうした混乱を踏まえて Standard C は、function-like マクロの呼び出しを 関数呼び出しに代替できるものとして位置付けることで、文法を整理したと考え られます。Rationale は Standard C のマクロに関する規定がよりどころとした 原則をいくつか挙げている中で、こういう原則を示しています。* ・関数を使えるところではどこでもマクロを使えるようにすること。 ・マクロ呼び出しはどこにあっても、すなわち地の文の中にあっても、マクロ の引数中にあっても、マクロ定義の中にあっても、同じ token sequence を 生成するようにマクロ展開を規定すること。 これは関数呼び出しなら当然のことですが、しかし引数付きマクロの呼び出し では当然ではなかったのです。エディタ様テキスト置換の繰り返しでは、こうな らないことは明らかです。 * C89 Rationale 3.8.3 Macro replacement C99 Rationale 6.10.3 Macro replacement [1.6.2] 引数は置き換えの前に展開される 関数呼び出しとパラレルなマクロ展開という原則を実現するために肝心なのは、 引数中のマクロを先に展開してからパラメータを引数に置き換えることです。そ して、そのためには引数中のマクロ呼び出しはその引数中で完結していなければ なりません(完結していない場合はエラーとなる)。引数中のマクロがその後ろ のテキストを食ってしまうなどということがあってはなりません。それによって、 ネストされた関数様マクロの呼び出しも論理的な明快さを保つことができます。 * * [2.4.25] 参照。 [1.6.2.1] #, ## 演算子の operand は展開されない ただし、# 演算子の operand はマクロ展開されないことになっています。ま た、## 演算子の operand もマクロ展開されず、連結によって生成された pp- token は再走査時にマクロ展開の対象となります。この規定はなぜ必要なのでし ょうか? この規定が意味を持つのは、もちろん引数がマクロを含んでいる場合です。マ クロを含む token sequence をそのまま文字列化したり連結したりしたい場合に、 この規定が役立つことになります。逆に展開してから文字列化したり連結したり したい場合は、#, ## 演算子を使わないマクロをもう一つかぶせることになりま す。このどちらでもプログラマが選択できるようにするためには、#, ## 演算子 の operand はマクロ展開しないという規定が必要なのです。* * [2.4.25.4, 2.4.25.5], misc.t / PART 3, 5 参照。# 演算子の operand がマクロ展開されないという規定が役に立つ代表的な例は assert() マクロ である。[4.1.2] 参照。 [1.6.3] 再走査とは さて、マクロ展開はマクロ呼び出しが置換リストに置き換えられ、function- like マクロの引数が原則として展開されてから置換リスト中のパラメータと置 き換えられた後、さらに置換リスト中のマクロ呼び出しをさがして再走査される ことになっています。 この再走査は K&R 1st. 以来の仕様です。そして、その背景には「エディタ様 のテキスト置換の繰り返し」という発想があったと思われます。しかし、 Standard C では function-like マクロの引数は ## 演算子の operand 以外は 先に完全に展開されてしまっています。再走査ではいったい何を展開するのでし ょうか? 再走査が必要なのは、置換リスト中のパラメータ以外の部分にマクロがある場 合、および ## 演算子を含むマクロの場合、もう一つはマクロの「定義」が何重 にもなったいわゆる cascaded macro です。マクロ呼び出しの「引数」が何重に もネストされている場合は、それらは再走査の前に入れ子構造で展開されている ので、通常は再走査で新たに展開されることはありません(しかし、例外はある。 [1.7.6], [2.4.27] 参照)。 [1.6.4] 同一マクロの再帰的展開の防止 Cascaded macro は次々と展開されていきますが、しかし、そうなっては困る 場合があります。それはマクロ定義自身が再帰的になっている場合です。これを そのまま展開すると無限再帰に陥ってしまいます。定義自身にそのマクロが含ま れている直接再帰の場合はもちろん、2つ以上の定義が間接的に再帰を成してい る場合も同じ問題をひき起こします。これを避けるため、Standard C では「展 開途中のマクロと同じ名前のマクロが置換リスト中に現れても、それは置換しな い」という規定を付加しています。規定の文章は難解ですが、その意図はわかり やすいものです。 これは関数様マクロの場合、関数とは違った文法になる点です。エディタ様置 換ともまた違っています。マクロ固有の仕様であり、マクロでしかできない便利 な処理として従来も使われてきたものなので、この仕様を明確化して残すのは妥 当なところでしょう。 [1.7] 問題点 さて、以上では Standard C のプリプロセス規定の良い面、単純明快な面だけ を取り上げてきましたが、詳細に検討すると、不規則なところや、処理系のオー バーヘッドの割に有用性や移植性に乏しいところがあちこちに含まれています。 これらの多くは pre-Standard の伝統的・暗黙的なプリプロセス方法を精算しき れずに残ってしまったものだと思われますが、こうした役に立たない盲腸的部分 の存在が、仕様をわかりにくくし、実装をやっかいにしています。また、 Standard C によって新たに無用に煩雑になったところもわずかながらあります。 以下にそれらの問題点を洗い出してみます。 [1.7.1] の形の header-name <, > で囲まれた形の header-name は K&R 1st. 以来伝統的に使われてきてい るものですが、トークンとしてはきわめて例外的な不規則なものです。このため Standard C では header-name という pp-token に関しては undefined な部分 と implementation-defined な部分が多くなってしまっています。例えば /* と いう sequence が含まれていた場合は undefined です。また、 のよ うに、header-name でなければ <, stdio, ., h, > といくつもの pp-token に 分解されるところを、#include 行に限って1つの pp-token に合成しなければ なりません。その方法は implementation-defined です。Tokenization は translation phase 3 で行われるのに、phase 4 でその行が #include directive であるとわかると、tokenization をやりなおさなければならないの です。はなはだ非論理的な仕様と言わざるをえません。Phase 3 でいったん分解 された(仮の)pp-token の間に space がある場合の処理も処理系定義となりま す。#include などというディレクティブは最も portability の高い もののように見えますが、プリプロセスの実装を考えるとかなり portability の低いものなのです。#include 行の引数がマクロであると、さらに不規則性が 増します。 ", " で囲まれた形式の header-name には、これらの問題はありません。ただ し、\ を escape 文字として扱わないというところは <, > による header-name と同様で、文字列リテラルとは違った面を持っています。しかし、escape sequence は phase 6 で処理されるものなので、phase 4 で処理されてしまう header-name に escape sequence が存在しないのは、非論理的なことではあり ません(規格では header-name 中の \ は undefined である。これは実装を楽 にするための配慮であろう。実際、", " の中では \ が " の直前にあるのでな い限り、問題は生じない(*)。<, > の中ではもう少し複雑であるが)。 また、#include と #include "stdio.h" との違いは、前者は処理 系定義の特定の場所だけを探すのに対して、後者はカレントディレクトリ(から の相対パス)を先に探して、無ければ と同じ場所を探す、というだ けのことにすぎません(Standard C ではOSに関する前提を置いていないので 「カレントディレクトリ」という用語は使っていないが、多くのOSについては そう解釈される)。すなわち、#include は #include "stdio.h" と 書けばすむことなのです。 Header-name の形式に2種あることは、ユーザ定義のヘッダとシステムの提供 するヘッダとの区別が一目でわかるという readability の利点がありますが、 そのためにはわざわざ不規則なトークンを用意しなくても、"stdio.h" と "usr.H" というふうに suffix で区別すれば足ります(念のために付言すると、 これは readability の問題であるから、システムがファイル名の大文字と小文 字を区別しなくてもかまわない。もちろん、"usr.hdr", "usr.usr", "usr.u" 等 でもかまわない)。 <, > で囲まれた形の header-name は言語仕様としては無用であり、プリプロ セスの tokenization を面倒にするだけなので、廃止したほうが良いと思います。 過去のソースをコンパイルするためにはいきなり廃止するわけにはゆきませんが、 obsolescent feature と規定してほしいものです。 * ところが、C99 では \ で始まる UCN が導入されたために、少々やっかい になった。 [1.7.2] Character-base のなごりを残す # 演算子の規定 次の問題は、# 演算子の operand 中の pp-token の間の token separator と しての white spaces の扱いです。1つ以上の white spaces は1つの space に圧縮し、1つもない場合は space を挿入しないことになっています。 これは中途半端な規定です。Token-based な動作に徹底するためには、token separator の有無に左右されないことが必要であり、そのためには token separator をすべて削除するか、それともあらゆる pp-token の間に one space を置くか、どちらかに規定すべきでした。C89 Rationale 3.8.3.2 (C99 Rationale 6.10.3.2) The # operator はこの規定について、"As a compromise between token-based and character-based preprocessing discipline"(token- based なプリプロセス法と character-based なそれとの折衷案として)決定さ れたと述べています。 この折衷はプリプロセッサの実装を容易にするどころか、余計な負担をかける 結果になっており、また複雑なマクロの展開方法にあいまいさをもたらしていま す。ANSI C 3.8.3 (C90 6.8.3, C99 6.10.3) Macro replacement マクロ置換え -- Examples 例4. にこういう例が載っています。 #define str(s) # s #define xstr(s) str(s) #define INCFILE(n) vers ## n #include xstr(INCFILE(2).h) この #include 行はこう展開されるというのです。 #include "vers2.h" この例は多くの問題をはらんでいます。INCFILE(2) が vers2 に置換されるこ とにはあいまいさはありません。しかし、xstr() の引数である INCFILE(2).h の展開結果は vers2, ., h という3つの pp-token の sequence です。規格書 の展開例はこの3つの pp-token の間に white spaces がないものとして扱って います。ここには次のような問題があります。 1.vers2 はソース中にあった pp-token ではなく、マクロ置換によって生成 されたものである。vers2 の後ろに white spaces がないことを保証するために は、マクロ置換がその前後に white spaces を生成しないようにしなければなら ない。しかし、マクロ置換が常にそうであると、少なくともプリプロセッサがコ ンパイラから独立したプログラムである場合、マクロ展開の結果として pp- token が暗黙のうちに連結してしまうことが発生する。これは token-based な プリプロセスの原則に反する。 2.# 演算子の operand になりうるマクロの置換で前後に white spaces を 生成せず、しかも pp-token が暗黙のうちに連結するのを防ぐためには、関数様 マクロ呼び出しの引数中に存在するマクロの置換では、内部的に仮の white space で置換結果をくるんでおき、それが # 演算子の operand になった場合は 仮の white space を削除し、すべての置換が終わってから最後に残った仮の white space だけを本物の space に置き換える、といった小細工が必要になる (*1)。これはプリプロセスの実装にとってかなりの負担である。そして、それに 見合うメリットがない。しかも、こうした処理が必要であることは規格書の本文 からは明らかではなく、何が正しい処理であるのかが不明確である。 こうしたあいまいさないしややこしさはすべて、# 演算子の operand 中の token separator の扱いの中途半端さに由来しています。 # 演算子ではすべての pp-token を single space で分離した上で文字列化す るという仕様が、pp-token の暗黙の連結を防ぎ、ややこしい問題を発生させる ことがなく、文字列化された引数がどういう pp-token sequence であったかが わかるので、良いと思われます。そう規定すると、このマクロは "vers2 . h" と展開されることになります。もちろん、これは適切なマクロではありません。 この例でわかるように、space のないところにそれが挿入されては困るほとん ど唯一の場合が、#include 行のマクロで #, ## 演算子が使われた場合です。 Translation phase 4 で処理される #include 行では、phase 6 で処理される文 字列リテラルの連結は使えません。しかし、#include 行のマクロは強いて #, ## 演算子を使ってパラメータ化しなくても、単純に文字列リテラルで定義すれ ばそれですむことです。たったこれだけのために token-based な原則を崩すの は、あまりにもバランスを失しています。 Standard C のプリプロセス規定では syntax は token-based なものでありな がら、# 演算子の semantics のところで突然 character-based な規定が現れて、 論理的な一貫性を損なっています。*2 その上、規格書のこの例は規格本文からは必ずしも明らかではない仕様を前提 としています。不適切な例であり、削除すべきでしょう。 *1 MCPP もしょうがないのでそうしている。 *2 JIS C は ISO C を翻訳しただけのもので、内容に変更は加えていないこ とになっている。しかし、「JIS ハンドブック」の X 3010 / 6.8.3 例4. の印刷には、fputs( ..) というマクロの展開結果が原文とは space の有無 が違っているというミスがある。この印刷は space の扱いに無神経であり、 # 演算子の規定をよく理解している人が原稿あるいは印刷をチェックしたと は思えない。しかし、規定そのものが無用に煩雑であることも確かである。 [1.7.3] マクロ再定義時の white spaces の扱い # 演算子の operand 中の white spaces の扱いと同様の規定がマクロ再定義 に関してもあります。−−マクロの再定義は元のマクロと等価なものでなければ ならない。等価であるためには、パラメータの数と名前が同じで、置換リストも 同じ spelling でなければならない。ただし、置換リスト中の white spaces に ついては、その有無は同じでなければならないが、数は違っていても良い。−− と規定されています。 # 演算子の規定が上記のようであれば、置換リスト中の white spaces につい ても同じ扱いをしなければならないので、これは当然の帰結です。やはり、問題 の由来は # 演算子の規定にあります。 # 演算子は operand 中の pp-token の間にはすべて one space があるものと して扱うということにすれば、マクロの再定義に際しても white spaces の有無 は問題にならなくなります。 さらに、プリプロセッサの実装ではこれを一般化して、ソース中のすべての pp-token の間を原則として one space に置き換えるようにすることもできます。 こうすることによって、マクロ展開に際しての tokenization を簡単かつ正確に 行うことができます。ただし、この原則の例外が2つあります。1つは preprocessing directive 行での改行コードであり、もう1つはマクロ定義での マクロ名とそれに続く '(' の間の white spaces の有無です。こればかりは伝 統的にCのプリプロセスの根幹を成しているものであり、いまさら変えるわけに はいきません。 [1.7.4] 関数様マクロ再定義時のパラメータ名 マクロの再定義に際してはパラメータの名前も一致していなければならないと 規定されていることは [1.7.3] で触れましたが、これは過剰な規定だと思われ ます。パラメータ名は、もちろんマクロの展開に何の違いももたらしません。し かし、再定義に際してこれをチェックするためには、プリプロセッサはすべての マクロ定義のパラメータ名を記憶しておく必要があります。ところが、記憶して もその使い道は、少なくとも規定の範囲では、再定義のチェック以外に何もあり ません。たったこれだけのほとんど意味のないチェックのために、処理系にバカ にならないオーバーヘッドをかけるのは、感心しません。 マクロの再定義ではパラメータ名も一致していなければならないという規定は、 削除したほうが良いと思います。 [1.7.5] 何を評価するのかわからない #if 式の文字定数 #if 行の引数である #if 式は整数型の定数式ですが、その評価はプリプロセ スで行われるので実行時環境から独立したものでなければなりません。そのため、 一般の整数定数式から、実行時環境に対する問い合わせを必要とするキャスト、 sizeof 演算子、列挙定数が除外されています(これらは translation phase 7 で初めて評価される)。しかし、文字定数(およびワイド文字定数)は除外され ていません。 文字定数の評価は次のように何重にも処理系定義であり、portability はほと んどありません。 1.基本文字の値さえも基本文字セット(ASCII, EBCDIC 等)によって異なる。 2.基本文字セットが同じであっても、single-character character con- stant さえも符号の扱いは処理系定義である(コンパイラ本体では char が符号 なしか符号つきかによる)。 3.Multi-character character constant の評価は処理系定義であり、基本 文字セットと符号の扱いが同じでも、値が同じとは限らない。CHAR_BIT が8で char が符号なしだとしても、'ab' が 'a' * 256 + 'b' となるのか、それとも 'a' + 'b' * 256 となるのかは、規定されていない。 4.Multi-byte character の encoding は処理系定義である。Wide char- acter の encoding は multi-byte character の encoding に従う。wchar_t の サイズも符号の有無も処理系定義である。 5.Multi-byte character の encoding が同じであっても、その値の評価は 同じとは限らない。3と同じ問題がある。 6.以上はいずれもコンパイラ本体での文字定数の評価にも共通する問題であ るが、さらにプリプロセスでの文字セットはコンパイラ本体と違っていてもよい ことになっている。 Translation phase 4 までが対象とするのはソース文字セットであり、phase 6 以降が対象とするのは実行時文字セットである。phase 5 が文字定数と文字列 リテラル中の文字のソース文字セットから実行時文字セットへの変換を行う。す なわち、基本文字セットも、multi-byte character encoding も、ソースと実行 時とで一方または双方が違っているかもしれない。 #if 式の文字定数が評価されるのは phase 4 であるが、これはソース文字セ ットの値でも実行時文字セットの値をシミュレートしても、どちらでもよいこと になっている。ソース文字セットとも決められていない。 7.#if で評価する文字セットと実行時文字セットが同じであっても、その評 価のしかたは違っていてもよいことになっている。すなわち、符号の扱いも、 multi-character character constant, multi-byte character constant の評価 のバイトオーダーも、phase 4 と phase 7 とで違っているかもしれない。 8.しかも、文字定数の評価の型は、phase 7 では multi-byte character を 含めて character constant は int、wide character constant は wchar_t で あるのに対して、phase 4 では C90 では long または unsigned long である。 すなわち、phase 4 では int は long と同じ内部表現を持つかのように扱われ、 unsigned int は unsigned long と同じ内部表現を持つかのように扱われる。し たがって、文字セット、符号の扱い、評価のバイトオーダーが phase 4 と 7 と でまったく同じであっても、INT_MAX < LONG_MAX の処理系では、phase 4 でオ ーバーフローしない文字定数が phase 7 ではオーバーフローすることもありう るし、phase 7 で int の負数になるものは phase 4 では long の正数になるか もしれず、正数になるか負数になるかさえも決まってはいない。文字定数でない 整数定数トークンは負数になることはないが、文字定数については正負は一般的 にはほとんど予測できない。 9.C99 では #if 式の型はその処理系の最大の整数型とされた。すなわち、 評価の型も処理系によって異なるかもしれない。 10.さらにプリプロセスだけでなくコンパイルにも共通する問題であるが、 multi-byte character には encoding の問題がある。たとえば UTF-8 は2バイ トの unicode の文字を1バイトないし3バイトで encode するが、その文字定 数の「値」とはいったい何であろうか? 漢字の値は UTF-8 の3バイトの sequence を評価した「値」であろうか、それとも元の unicode の「値」であろ うか。これも処理系定義ということになるのであろうが、どういう仕様が合理的 であるのかも判然としない。 11.これもコンパイルと共通する問題であるが、C99, C++98 では UCN とい うものまで導入された。同じ文字を UCN で表記したものと multi-byte character として書いたものとは同じ「文字」であろうか。本来は同じ文字のは ずであるが、その「値」は multi-byte character の encoding によってそれぞ れ違ってくるであろう。 こういうことで、#if 式の文字定数の値は処理系間での portability もなけ れば、同じ処理系のコンパイルフェーズとの間でも違うかもしれないところが多 く、どう評価されるかほとんど予測できません。 一般にはC言語の整数型の規定にはあいまいな部分は少なく、演算に関しては 負数の扱いが処理系定義であるものの、それは CPU しだいで決まるものであり、 処理系作成者が任意に決めることのできる部分はあまりないのですが、文字定数 の評価だけが例外です。これは CPU の仕様、基本文字セット、multi-byte character encoding というシステム側の要因で決まる側面のほかに、処理系作 成者の裁量にまかされている面が多くあります。 これが #if 式の文字定数となると、処理系の裁量の範囲がさらに大幅に増え、 コンパイルフェーズとの一致は保証されず、これを評価してもいったい何を評価 したのかほとんどわかりません。文字定数の評価は本来は実行時環境への問い合 わせを必要とするものだと考えられます。Standard C のプリプロセスではこの 問い合わせを必要とする処理は除外されたにもかかわらず、なぜか文字定数だけ は除外されませんでした。そして、問い合わせをしないでもすむ規定を無理に作 ったために、意味不明なものとなってしまったと思われます。 こういう #if 式の文字定数にどんな使い道があるのでしょうか? コンパイル フェーズでは char 型変数の値を文字定数と比較することがよく行われますが、 変数が使えないプリプロセスフェーズではその使い道もありません。#if 式の文 字定数の使い道としては、私には適切な例は思い付きません。これは無用の長物 であり、キャストや sizeof と同様に、#if 式の対象から除外すべきでしょう。 除外しても、キャストや sizeof が除外されたのに比べれば、困るソースははる かに少ないはずです。 [1.7.6] マクロ再走査の関数様でない規定 マクロ呼び出しはいったん置換リストに置き換えられた後、それがさらに再走 査されますが、Standard C の再走査の規定できわめて汚いのは、そのマクロ呼 び出しの後ろのトークン列があたかも置換リストの続きを成しているかのように、 置換リストと連続して走査されることです。これは関数呼び出しをモデルとした 関数様マクロの規定の原則からまったく逸脱しており、マクロ展開をわかりにく くする最大の要因となっています。私は、後続のトークン列も再走査の対象とす るというこの規定は削除し、再走査の対象は置換リストだけに限定すべきだと考 えています。 実は後続のトークン列も再走査の対象とするのは、おそらく K&R 1st. のころ からの長年の暗黙の仕様だったと思われます。Standard C ではこの仕様は必要 なくなったはずなのですが、しかし盲腸のように生き残ってしまっているのです。 これはマクロ展開の根幹にかかわる問題であるので、以下に詳細に検討すること にします。 マクロ rescan(再走査)の方法を文章で正確に記述するのは、容易なことで はありません。規格書の文章も、K&R 2nd. 等の文章もわかりやすいものではあ りません。たとえば、K&R 2nd. A.12 には「置換リストは繰り返し再走査される」 とあります。しかし、規格書には繰り返すとは書かれていません。Rescan は1 回だけ行われるようにも読めます。再帰的に行われるようにも読めますが、そう 明記されているわけではありません。 これは実例を使わないと、正確に説明することができないのではないかと思わ れます。さらに、実装方法も説明しないと、直観的に理解できるようにはならな いでしょう。そのくらい、マクロの rescanning というものはマクロ展開の伝統 的な実装方法に密着したものなのです。 まず、次のバカげた例を検討してみます。問題を単純にするために、この x, y はマクロではないとします。このマクロ呼び出しはどう展開されるのでしょう か? #define FUNC1( a, b) ((a) + (b)) #define FUNC2( a, b) FUNC1 OP_LPA a OP_CMA b OP_RPA #define OP_LPA ( #define OP_RPA ) #define OP_CMA , FUNC2( x, y); 1: FUNC2( x, y) 2: FUNC1 OP_LPA x OP_CMA y OP_RPA 3: FUNC1 ( x , y ) 1: が 2: に置換され、2: の rescan によって 3: が生成されることは、すぐ わかります。では、3: はマクロ呼び出しでしょうか? すなわち、これは先頭か らもう一度 rescan されるべきでしょうか? Rescan は先頭から何度でも繰り返されるものなのでしょうか、それとも対象 範囲が再帰的にしだいに狭められてゆくものなのでしょうか? 実は、そのどち らでもないのです。 Rescan というものの実際は伝統的にある種の変則的な再帰で、あるいはそれ と同じ結果になるある種の繰り返しで行われてきたと思われますが、その古典的 な例が Kernighan & Plauger "Software Tools"(「ソフトウェア作法」)の Macro Processing の章に載っているものです。これは後に M4 マクロプロセッ サに発展してゆくもので、これ自体はCプリプロセッサではありませんが、 Ritchie がCで書いたマクロプロセッサから「盗んだ」ものだと書いてあり、C プリプロセッサの実装方法の原型をうかがい知ることができます。 このマクロプロセッサでは、マクロ呼び出しがあるとその置換リストを入力に 送り返して読み直すという方法で rescanning を実現しています。置換リスト中 にまたマクロ呼び出しがあった時は、「それが元の入力にあったかのように」そ の新たなマクロの置換リストを同じように送り返して読み直します。「これはマ クロの置換テキストを再走査するエレガントな方法を提供する」「入力の入れ子 構造に内在する再帰を、われわれはこの手で処理しようというのである」等とあ るように、この方法はマクロプロセッサのプログラムを構造化しわかりやすくす るために、大いに役立っています。 Cプリプロセッサの多くもこれと同じように、疑似的な入力、すなわちある種 のスタックに置換リストを積んで、それを読み直すという方法でマクロの re- scan をしているものと思われます。 上記の例では、2: を rescan する時に、FUNC1 がこの時点ではマクロ呼び出 しでないことがわかると、同時にこのトークンが確定し、以後の置換は繰り返し にしろ再帰にしろ OP_LPA 以降が対象となります。OP_LPA が ( に置換されて、 それがマクロでないことがわかると、次は x 以降が対象となる、というふうに して先頭から順次、確定してゆき、3: が最終結果となります。これはもはやマ クロ呼び出しではありません。 "Software Tools" 以来の(あるいはそれ以前からの)この方法は確かに re- scan の簡明な実装方法です。しかし、"Software Tools" は触れてはいませんが、 そこにまた落とし穴もあります。問題は、入力に送り返された置換リストはソー スと連続して読み込まれるために、rescan が置換リストを越えて元のマクロ呼 び出しの後ろの部分までスキャンしてしまう可能性があることです。ネストされ たマクロでは、rescan しているうちにいつの間にかネストレベルがズレてきて しまうこともありえます。引数つきマクロの名前に展開される引数なしマクロや、 置換リストが新たな引数つきマクロの呼び出しの前半部分を構成するという変態 マクロが、この事態をひき起こします。 #define add( x, y) ((x) + (y)) #define head add( head a, b) これがその例です。この奇妙なマクロ呼び出しは ((a) + (b)) と展開されま す。あろうことか、Standard C ではこれが公式に認知されてしまいました。つ まり、このマクロは undefined どころか合法的なものなのです。 本来、Cプリプロセッサはまさかこんな変態マクロの展開を意図していたとは 思えません。しかし、たぶん原初のCプリプロセッサの実装が上記のようなもの であったために、こうしたマクロを黙ってそれらしく展開する結果となり、この 穴を意識的に利用するプログラムまで現れ、これが事実上の標準仕様となってし まい、ついに Standard C でもこれを追認することになったのではないでしょう か。すなわち、原初のCプリプロセッサの実装のささいな欠陥が、奇妙な de facto standard を誘導し、Standard C にまで尾を引いているのです。これが盲 腸の盲腸たるゆえんです。 さて、rescan は再帰か繰り返しかという話に戻ると、これは変則的な再帰で あり、繰り返しと言ってもあながち間違いではないものである、ということにな るかと思います。再帰は再帰ですが、普通の再帰のように対象範囲がしだいに狭 められてゆくとは限らず、むしろ対象範囲がしだいに後ろにズレてゆくという奇 妙な性質をもった再帰なのです。これは再帰でなくても繰り返しで実現すること もできます。ただし、先頭からの繰り返しではなく、途中からの繰り返しで、し だいに後ろの部分を取り込んでゆくものです。 したがって、ソースファイル中のコメントやプリプロセスディレクティブがす べて処理された後のテキストを考えると、このズレズレ rescan だけでテキスト の最初から最後まで通して処理してしまうことも可能です。実際、"Software Tools" ではそういう方法をとっており、現在のCプリプロセッサのソースにも 似たやり方をしているものがあります。すなわち、rescan とはマクロの展開 (expand)と同義であり、それはまたテキスト全体のマクロの展開とも同義なの です。 Rescan の対象がしだいに後ろにズレてゆくことは、多くの問題を引き起こし ます。次の例はどう展開されるべきかが不明確なマクロの例として C89 Rationale 3.8.3.4 (C99 Rationale 6.10.3.4) に載っているものですが、この 処理が規定されなかったのは「プリプロセスのこういう quirks(気まぐれ、奇 癖)までいちいち規定しても何の役にも立たないからだ」とされています。しか し、この例は示唆的です。これはむしろ規定することができなかったのです。 #define f(a) a*g #define g(a) f(a) f(2)(9) この例では、まず f(2) が 2*g に置換されます。「後続するトークン列」を rescan の対象としなければ、マクロ展開はこれでおしまいで、f(2)(9) は 2*g (9) というトークン列となります。ところが「後続するトークン列」も対象とさ れるために、この g(9) がマクロ呼び出しを形成してしまい、f(9) に置換され ます。ここで、この f(9) はさらに 9*g と置換されるべきか、それとも同名マ クロの再置換禁止の規定を適用して置換しないでおくべきかが不明確です。f(9) というトークン列は f(2) の最初の置換結果の後尾の g と「後続するトークン 列」である (9) のつながったものの rescan によって生成されたものであるの で、これが f(2) の呼び出しのネストの中にあるのか外にあるのかが判然としな いからです。 この問題については C90 Corrigendum 1 で訂正が行われました。Annex G.2 Undefined behavior に次の例を追加するというのです。 -- A fully expanded macro replacement list contains a function-like macro name as its last preprocessing token (6.8.3). -- 完全に展開されたマクロ置換リストの最後の前処理字句が関数形式マクロ の名前である場合 (6.8.3)。 しかし、この訂正は混乱に輪をかけるものでしかありません。 まず、"fully expanded macro replacement list" という文言が意味不明です。 これは「引数があれば引数中のマクロが展開された後の置換リスト」と解釈する しかありませんが、そうすると f(2)(9) の例では、同名マクロの再置換を云々 する前に、f(2) が 2*g に置換され、それを再走査して g が function-like マ クロの名前であるとわかったところですでに undefined となります。すなわち、 この f と g のマクロ定義では、f の呼び出しがあると必ず undefined となる のです。 この「訂正」を適用すると、ISO/IEC 9899:1990 6.8.3 Examples にある次の ようなマクロ再走査の例示が、そもそも undefined となってしまいます。 #define f(a) f(x * (a)) #define x 2 #define g f #define w 0,1 #define t(a) a t(t(g)(0) + t)(1); /* f(2 * (0)) + t(1); */ g(x+(3,4)-w) /* f(2 * (2+(3,4)-0,1)) */ 規格書はこれらのマクロ呼び出しはコメントに書いたように展開されるとして いますが、Corrigendum を適用するとそうはなりません。この f と g のマクロ 定義では、g という identifier が出てくると必ず undefined となります。g の置換リストでは function-like マクロの名前である f が唯一で最後の pp- token ですから。 t(t(g)(0) + t)(1) これはまず、初めの t の呼び出しの引数が展開されます。 t(g)(0) + t ここにまた t(g) というマクロ呼び出しがあるので、これが展開されますが、 そのためにはまず引数を展開しなければなりません。 g そしてこれが f に置換されると、ここで undefined となるのです。 もしこれをこのままにして、さらに置換を続けても、 t(f) f となり、t(f) の展開結果の最後の pp-token が f であるので、再び undefined です。さらに置換を続けると、こうなります。 f(0) + t f(x * (0)) f(2 * (0)) f(2 * (0)) + t t(f(2 * (0)) + t) f(2 * (0)) + t これで初めの t の呼び出しの展開が一応終わりますが、この置換リストの最 後がやはり t という function-like マクロの名前であるので、三たび undefined となります。 では、次はどうでしょうか。 g(x+(3,4)-w) これも g が f に置換されたところですでに undefined です。 Examples と G.2 とが矛盾するという混乱に陥ってしまっています。 もし Examples のこれらの例が取り消されたとしても、この Corrigendum の 訂正では決して混乱は解消しません。第一に、G.2 は規格本文ではなく、この追 加は本文中に根拠を持たないものです。本文では「後続するトークン列」も rescan の対象とするとしか書いてないのです。第二に、もしこの Corrigendum を本文中に取り込んだとしても、先の例の #define head add( は add が置換リストの最後ではないので正しく #define head add は undefined というのでは、あまりにもアンバランスです。"fully expanded" という文言が意味不明であるという問題もあります。*1, *2 これが、「後続するトークン列」も rescan の対象とするという規定によって もたらされた quirks であることは、言うまでもありません。つじつまを合わせ ようとすればするほど、混乱してしまうのです。規格書には同名マクロ再置換禁 止の規定がきわめて難解な文章で書かれていますが、この難解さの一因もここに あります。 他方で Standard C は function-like マクロの呼び出しに際しては、引数中 のマクロの展開はその引数の中だけで行うと規定しています。引数中のマクロの 展開がその後ろのテキストまで食ってしまったのでは大混乱ですから、この規定 は当然と言えば当然です。 しかし、この結果、同じマクロでも引数中にある時とそうでない時とで違った 結果になるというアンバランスが発生します。 #define add( x, y) ((x) + (y)) #define head add( #define quirk1( w, x, y) w x, y) #define quirk2( x, y) head x, y) head a, b); quirk1( head, a, b); quirk2( a, b); この quirk1() の呼び出しでは、第1引数である head が add( に置換された 後、その rescan で完結しないマクロ呼び出しとして violation of constraint、 平たく言えばエラーとなるはずです。しかし、quirk2() および head a, b) は エラーにならずに ((a) + (b)) と展開されることになります。 しつこいようですが、こうしたバカげたことはすべて、一般にマクロの re- scan で「後続のトークン列」までが対象となってしまうことから発生している のです。実装上は、引数の展開が他のテキスト部分から独立して行われるために は、たとえ置換リストを入力に送り返す方法であっても、どのネストレベルであ るかという情報を付加する必要があります。それを使えば一般にも「後続のトー クン列」を rescan の対象に取り込まないようにすることは容易に実現できるは ずです。むしろ、現在の中途半端な規定では、引数中とそうでない場合とで処理 を変える必要があり、実装にも余計な負担をかける結果になっています。 Cのマクロ展開は伝統的にエディタ様の文字列置換の痕跡をとどめてきました。 エディタ様の文字列置換が極端に高機能化・複雑化したのが pre-Standard のマ クロ展開だと言えるでしょう。 これに対して Standard C では、引数つきマクロにわざわざ function-like マクロという名前まで付けて、その呼び出しの構文を関数呼び出しに近付けよう としたと考えられます。引数中のマクロは完全に展開してからパラメータと置き 換えるという規定も、その展開は引数の中だけで行われるという規定も、この原 則に合ったものです。ところが、一般にマクロの rescan は後続のトークン列を 取り込むという規定が、この原則をブチコワシています。テキスト置換の繰り返 しという先祖の遺物です。 後続のトークン列を rescan の対象から除外してしまえば、マクロ展開は完全 に再帰的に、すなわち対象範囲が前方にも後方にも再帰のたびに狭められてゆく ように(少なくとも広げられることはないように)規定することができたのです。 そして、function-like マクロはその名にふさわしいマクロとして、明快なもの になったことでしょう。そう決められては困るというソースはめったにあるとは 思われないので、そうならなかったのは単に ANSI C 委員会が、先祖から受け継 いだ盲腸を切り落とす決断がつかなかったからとしか考えられません。*3 C99 ではこれをスッパリと切り落としてもらいたかったのですが、またもやこ の盲腸は生き残ってしまいました。 *1 後者のような function-like マクロの名前に展開される object-like マ クロというのは、実際のプログラムでも時々目にするものである。次のよう なものである。 #define add( x, y) ((x) + (y)) #define sub( x, y) ((x) - (y)) #define OP add OP( x, y); これは前者のような function-like マクロの呼び出しの前半部分に展開さ れるものほど変態的なものではないが、こうしなければならない理由は何も ない。Function-like マクロのネストは次のように function-like マクロ でするのが良い。 #define OP( x, y) add( x, y) *2 この Corrigendum による正誤訂正が出てきた理由は、C90 ISO C 委員会 (SC 22 / WG 14)の "Record of Responses to Defect Reports" という文 書でわかる(#017 / Question 19)。ANSI C Rationale 3.8.3.4 の f(2)(9) のマクロ展開に関する議論がここで蒸し返されたのである。この例の直接の 問題は、「同名マクロ再置換禁止」の規定の適用範囲だったはずであるが、 委員会は同名マクロに限らない一般的な問題として回答してしまったのであ る。しかし、この解釈が Examples との間に矛盾を発生することには気が付 かなかったようである。 さらに、この "fully expanded" という文言が奇妙である。f(2) が 2*g に 置換され、g まで再走査された時、これは fully expanded なのであろうか? もしそうなら、それ以上の置換は起こらないから、undefined にもならない はずである。もしまだ fully expanded でないなら、g が後続の (9) とと もに再走査され、f(9) に置換される。もしこれが fully expanded である なら、2*f(9) の最後の pp-token は function-like マクロの名前ではない から、この回答はあてはまらない。すなわち、マクロ展開はいつ終わるのか が問題となっているところで、「マクロ展開が終わったら」と言っているの である。かくして、マクロ展開はいつ終わるのか、ますますわからなくなっ てしまったのである。 C99 1997/11 draft では Corrigendum のこの項目が Annex K.2 Undefined behavior に取り込まれていたが、1998/08 draft では削除されて、替わっ て Annex J.1 Unspecified behavior に次のような一節が追加された。そし て、結局これが C99 に採用された。 When a fully expanded macro replacement list contains a function- like macro name as its last preprocessing token and the next preprocessing token from the source file is a (, and the fully expanded replacement of that macro ends with the name of the first macro and the next preprocessing token from the source file is again a (, whether that is considered a nested replacement. 委員会はようやく Corrigendum の矛盾に気づいたようである。しかし、規 格本文の根本的な問題はそのままである。また、マクロ展開がいつ終わるの かも、結局は unspecified である。しかも、'(' がソース中にあるかどう かで区別するというのでは、同じマクロがソース中にある時と他のマクロの 置換リスト中にある時とで結果が違ってくるということであり、一貫しない 仕様である。 この問題については、[2.4.26] も参照のこと。 なお、C++ Standard ではマクロ展開に関する規定は C90 と同じであり、 C90 の Corrigendum 1 に相当するものも C99 の Annex J.1 で追加された 規定もない。 *3 こう決めても、先の FUNC2( x, y) はもしこれが他のマクロ呼び出しの引 数中にあった場合は、引数の展開で FUNC1 ( x, y) となり、さらに元のマ クロの rescan で ((x) + (y)) と展開されることになる。すなわち、引数 中にある場合とそうでない場合とで最終的な展開結果が異なる。しかし、こ れはまた別の次元の問題であり、不都合ではないであろう。 [1.7.7] C90 Corrigendum 1, 2, Amendment 1 での追加 ISO/IEC 9899:1990 については、1994 に Corrigendum 1(正誤訂正)が、 1995 に Amendment 1(追補)が出され、さらに 1996 に Corrigendum 2 が出さ れました。 Corrigendum 1 のほうはささいな文言の訂正がほとんどですが、2つだけプリ プロセスに影響を与えるものが含まれています。その1つは前記 [1.7.6] のマ クロの再走査に関するものです。 もう1つはマクロ定義中のマクロ名に $ 等が含まれていた場合についての、 きめわて特殊な規定です。 Standard C では identifier 中の文字として $ は認めていませんが、これを 認める有力な処理系も伝統的に存在しています。test-t/e_18_4.t にある 18.9 の例は、Standard C では $ が1文字で1つの pp-token と解釈されるので、マ クロ名は THIS で $ 以降が object-like マクロの置換リストとなり、THIS$AND $THAT という名前の function-like マクロというプログラムの意図とはまった く違った結果になります。 Corrigendum 1 では、こうした例に関して例外的規定が追加されました。すな わち、「object-like マクロの置換リストが non-basic character で始まる場 合は、マクロ名と置換リストとは white-space で分離されていなければならな い」というものです。この 18.9 の例に対しては Standard C は診断メッセージ を出さなければならないのです。それによって、$ や @ がマクロ名中に使われ ているソースが黙って意図しない結果にプリプロセスされる事態を防ごうという わけです。苦心の規定ですが、こうした例外が増えるのは困ったものです。$ や @ を identifier 中に認めない処理系では、こうしたマクロはたとえプリプロセ スでエラーとならなくても、コンパイルフェーズで必ずエラーになるはずなので、 この例外規定の必要はないと思われます。*1 このほか、ISO 9899:1990 では header-name という pp-token は #include directive にしか現れてはならないという意味不明な constraint がありました が、Corrigendum 1 で、header-name は #include directive でしか認識されな いと訂正されました。 Amendment 1 は multi-byte character, wide character およびそれらの文字 列を操作するライブラリ関数の追加が中心で、それに伴って , という標準ヘッダが追加されました。また、ISO 646 の文字セット に含まれない文字やそれを使った token, pp-token を表記する方法として、 trigraph と並ぶもう1つの選択肢として という標準ヘッダと digraph の規定が追加されました。 はいくつかの operator をマク ロで定義するごく簡単なヘッダで、特に問題はありません。*2 問題は digraph です。これは trigraph とよく似ていて、用途はほとんど同 じなのですが、プリプロセスでの位置づけはまったく違っています。Trigraph は character であり、translation phase 1 で通常の character に変換される のに対して、digraph は token, pp-token なのです。Digraph sequence が # 演算子によって文字列化される場合は、変換せずにそのまま文字列化しなければ なりません(この # そのものも digraph では %: と書くのであるが)。そのた め、そしてそれだけのために、処理系は少なくとも phase 4 が終わるまではこ れを pp-token として保っている必要があります。変換するとすれば、その後で す(文字列リテラル中の digraph ではなく、token として残った digraph sequence を変換する)。 これは処理系に無用の負担をかけるものです。Trigraph と同様に character として認識し、phase 1 で変換してしまうほうが、実装が簡明になります。これ を pp-token として保つ利点は何もありません。Amendment も、digraph と通常 の token との違いは文字列化される時にしか発生しないと注記しています。 Phase 1 で変換するようにすると "%:" といった文字列リテラルを書くのに困る という見方があるかもしれませんが、それは trigraph でも同様であり、取り上 げるにはあまりにも特殊な問題です。どうしても書きたければ "%" ":" とすれ ばすみます。Digraph は trigraph の alternative として phase 1 で変換する ように位置づけしなおすべきでしょう。 Corrigendum 2 にはプリプロセスに関する修正はありません。 *1 C99, C++ Standard ではこの規定は消えている。 C99 ではそのかわりに、6.10.3 Macro replacement / Constraints に次の 一般化した規定が付け加えられた。 There shall be white-space between the identifier and the replacement list in the definition of an object-like macro. これも tokenization の例外規定であるので、感心しない。 *2 C++ Standard では、これらの identifier 様 operator はマクロではな く token である。なぜそうしたのか理解しがたいが(プリプロセスすべき ことを極力減らすという発想か?)、とにかく処理系にとってはやっかいな ことである。 [1.7.8] 冗長な規定 ANSI C 2.1.1.2 (C90 5.1.1.2, C99 5.1.1.2) Translation phases(翻訳フェ ーズ)の 3 には、実害はないものの冗長な規定があります。 A source file shall not end in a partial preprocessing token or comment. Translation phase 2 で、ソースファイルは 無しでまたは で終わってはならないとあるので、phase 2 を通ったソ ースファイルは必ず のない で終わります。Partial preprocessing token で終わることは決してありません。Partial preprocessing token に類したものとしては、論理行内で " や ' や <, > の対 応のとれていないものがありますが、それは ANSI C 3.1 (C90 6.1) Lexical Elements(字句要素)/ Semantics(意味規則)で undefined とされているもの であり、またソースの末尾に限った問題ではありません。"partial preprocessing token or" は不要な文言です。 ANSI C 3.8.1 (C90 6.8.1, C99 6.10.1) Conditional inclusion(条件付き取 込み)/ Constraints(制約)には誤解を招く表現があります。 it shall not contain a cast; identifiers (including those lexically identical to keywords) are interpreted as described below; この "it shall not contain a cast; " は蛇足です。それに続く部分と Semantics(意味規則)で、keyword と同じ identifier を含むすべての identifier はマクロであれば展開され、残った identifier は 0 と評価される ことが明らかにされています。Cast をそれと並列して別に取り上げる必要はあ りません。(type) という構文では type は単なる identifier として扱われる ことが明らかです。 逆に、これが constraint にあると、処理系は cast の構文を認識して、それ に対する診断メッセージを出さなければならないとも、解釈されます。それは規 格書の意図ではないでしょう。Translation phase 4 では keyword は存在しな いので、cast は認識のしようがないのです。sizeof もこの点では同じですが、 ここで sizeof に言及せず、cast だけ取り上げているのも奇妙なことです。 こういう文言を「蛇足」と言います。 [1.8] C99 のプリプロセス規定 C99 では、プリプロセスに関しては次の仕様が追加されています。 1. 識別子、文字列リテラル、文字定数、pp-number の中の \uxxxx, \Uxxxxxxxx の形の 16 進 sequence は UCN (universal-character-name) と言 い、Unicode の文字の値を意味する。これは basic source character set に含 まれない extended character を指定するものでなければならない。# 演算子に よって UCN が文字列化された時に \ を重ねるかどうかは implementation- defined とする。 2. 識別子中に implementation-defined な文字を使うことができる。したが って、漢字のような multi-byte-characters を識別子中に使える imple- mentation も可能となった。 3. // から行末までをコメントとして扱う。 4. Pp-number の中に e+, E+, e-, E- と同様に p+, P+, p-, P- という sequence も認める。これは 0x1.FFFFFEp+128 というふうに、浮動小数点数のビ ットパターンを16進で表記するためのものである。 5. #if 式の型はその処理系の最大の整数型とする。long long / unsigned long long は必須であるので、#if 式の型は long long またはそれ以上のサイ ズとなる。 6. 引数が可変個のマクロが使える。 7. マクロ呼び出しのカラ引数は有効な引数とする。 8. 事前定義マクロ __STDC_HOSTED__ を追加する。これは hosted implementation であれば 1 に、そうでなければ 0 に定義される。事前定義マ クロ __STDC_VERSION__ は 199901L に定義する。 9. 事前定義マクロ __STDC_ISO_10646__, __STDC_IEC_559__, __STDC_IEC_559_COMPLEX__ をオプションとして追加する。 10. _Pragma operator を新設する。 11. #pragma STDC で始まるディレクティブ名を規格と処理系用に予約し、浮 動小数点演算の方式を表す3つの #pragma STDC ディレクティブを追加する。# pragma STDC で始まるディレクティブはマクロ展開の対象としないが、そうでな い #pragma 行はマクロ展開の対象とするかどうかは implementation-defined である。 12. Wide-character-string-literal と character-string-literal とが隣 接しているのは C90 では undefined であったが、これは Wide-character- string-literal として連結する。 13. #line の引数として使える行番号の範囲は [1,2147483647] に拡大する。 14. Translation limits を次のように引き上げる。 ソースの論理行の長さ : 4095 バイト 文字列リテラル、文字定数、header name の長さ : 4095 バイト 内部 identifier の長さ : 63 文字 #include のネスト : 15 レベル #if, #ifdef, #ifndef のネスト : 63 レベル 式のカッコのネスト : 63 レベル マクロのパラメータの数 : 127 個 定義できるマクロの数 : 4095 個 15. Header name は6文字 + . + 1文字までが保証されていたが、これを8 文字 + . + 1文字までに変更する。 可変引数マクロというのは、次のようなものです。 #define debug(...) fprintf(stderr, __VA_ARGS__) というマクロ定義があると、 debug( "X = %d\n", x); というマクロ呼び出しは次のように展開されます。 fprintf(stderr, "X = %d\n", x); すなわち、パラメータ・リスト中の ... が1個以上のパラメータを意味し、 置換リスト中の __VA_ARGS__ がそれに対応します。そして、マクロ呼び出し時 には ... に対応する引数が複数あっても、それらを , を含めて連結したものが 一つの引数のように扱われます。 C90 で undefined behavior とされているものの中には、十分意味のある解釈 の可能なものがあります。マクロ呼び出しのカラ引数などがそうで、これを 0 個の pp-token と解釈することが有用な場合があります。C99 では、これが有効 な引数とされました。 _Pragma( "foo bar") と書くと #pragma foo bar に変換される _Pragma とい う拡張演算子が C99 で取り上げられています。#pragma 行の引数は C90 ではマ クロ展開されず、マクロ展開の結果として生じた #pragma ディレクティブ類似 の行はディレクティブとして扱われず、マクロ定義の置換リスト中に #pragma を書くことができないのに対して、_Pragma 式はマクロの置換リスト中に書くこ とができ、その結果として生じた #pragma はディレクティブとして扱うという 拡張を施すことで、扱いにくい #pragma の portability を向上させようという ものです。 こうした変則的な拡張をしなくても、#pragma 行の引数はマクロ展開の対象と するという変更を加えるほうが簡明であり、それで portability 向上の意図は かなり達成できるはずですが、その場合はマクロの中に #pragma を書くことが できないという制約が残り、マクロ展開されてはいけない #pragma の引数はユ ーザ用名前空間と切り離すため __ で始まる名前に変更しなければならないとい う問題が出てきます。_Pragma() operator は変則的ではあるものの、実装が面 倒というほどでもなく、妥当な仕様かと思われます。 Unicode の導入には問題がありすぎます。まず、処理系は multi-byte character と Unicode との変換のために巨大な表を用意しなければならず、大 きな overhead を生じます。16 ビット以下のシステムでは事実上、実装不可能 です。Unicode をまったく扱っていないシステムも多く存在します。また、 Unicode と multi-byte character とでは一対一に mapping できない場合が多 々あります。プログラム言語の国際化という名目で Unicode をC言語の標準の 地位に置くのは、強引すぎると思われます。 C99 では 1997/11 draft や C++ Standard に比べて UCN の扱いが大幅に後退 し、プリプロセッサの負担は比較的小さくなりました。そのため、MCPP でも一 応の実装が可能になりました。*1 しかし、コンパイラ本体の負担は依然、大きいものがあります。また、可読性 のない表記であるので、trigraph と同様に、あまり使われないで終わることも 予想されます。*2 なお、MCPP では、-S1 -V199901L というオプションで C99 の仕様になります が、__STDC_ISO_10646__, __STDC_IEC_559__, __STDC_IEC_559_COMPLEX__ は事 前定義しません。これは処理系のヘッダファイルで定義することになるだろうか らです。 *1 1997/11 draft では C++ Standard とほぼ同様に、translation phase 1 で basic source character set に含まれない extended character はすべ て UCN に変換し、phase 5 で execution character set の文字に再変換す ることになっていた。 もしこれを実装する場合は、プリプロセスの前と後にこれらの変換をするツ ールを呼び出すということになると思われる。この変換は OS に依存するも のなので、別のツールにするほうが現実的である。 *2 C99 Rationale 5.2.1 Character sets によると、この仕様は処理系付属 のツールで multi-byte character のソースとの間で相互に変換して使うこ とを想定したものとされている。Multi-byte character の文字列リテラル の部分を切り離して別ファイルにまとめておいて処理するということであろ う。どこまで実用になるものであろうか。 [1.9] 明快なプリプロセス規定を 以上に述べてきた Standard C プリプロセス規定の問題点と私が考えるものは、 そのまま将来の Standard C への要望でもあります。まとめると、次のようなこ とになります。 1. の形式の header-name は obsolescent feature(廃止予定機 能)とする。次々期では header-name は文字列リテラル形式のものだけにする。 2.Token-based なプリプロセスの原則を貫き、# 演算子は引数中の token separator の有無に左右されないように、token separator がなくても pp- token 間にはすべて single space を挿入したうえで文字列化することにする。 3.同様に、マクロの再定義に際しては、置換リスト中の token separator の有無の違いは問題にしないことにする。 4.マクロの再定義に際してパラメータ名の違いをチェックすることは、処理 系のオーバーヘッドを増やすだけでほとんど有用性がないので、この違いは問題 にしないことにする。 5.文字定数の評価は本来は実行時環境への問い合わせを必要とするものであ り、#if 式では有用性もほとんどないので、これは #if 式の対象から除外する。 6.関数様マクロの関数様の扱いを徹底し、マクロ呼び出しが引数中にあって も置換リスト中にあっても原則として同じ pp-token sequence が生成されるよ うに、マクロの rescanning は置換リストだけを対象とし、マクロ呼び出しに続 く pp-token sequence は対象としないことにする。 7.Digraph は token ではなく、trigraph と同様の character の代替 spelling とし、translation phase 1 で変換することにする。 8.Translation phase 3 の規定のうち、"partial preprocessing token or" は削除する。 9.#if 式に関する "it shall not contain a cast; " という記載は con- straint から削除し、C99 の footnote 140 に吸収する。 10.Trigraphs はヨーロッパ大陸では実際に使われているのであろうか? もしかなり使われているのであれば残すしかないが、そうでなければ廃止したい。 これらはいずれも、不規則な規則を整理し、プリプロセス規定を単純明快にし ようとするものです。これによってプリプロセスがわかりやすくなることは間違 いないでしょう。逆に困ることはほとんどないはずです。 MCPP では、Standard モードでは Standard C の気に入らないところも含めて Standard C のプリプロセス規定を完全に実装しているつもりですが、post- Standard というモードでは以上の変更を加えたプリプロセスを実現しています (そのほか、UCN, identifier 中の multi-byte characters の使用も除外して いる)。 C90 の Amendment 1, Corrigendum 1 では、プリプロセスについては不規則な 規則を整理するよりは不規則性を増やす方向に動いてしまいました。 C99 でも、新機能がいろいろ追加されたばかりで、上記のような論理の混乱が どれも整理されなかったのは残念です。*1 C99 で追加された仕様については、次の点を要求したいと思います。 1.Unicode (UCN) の導入はオプションにとどめる。 なお、上記の問題点のほかに、#if 式に適用される整数型の演算規則について ももう一つ問題があります。 1.定数式の演算結果はその型で表現できる範囲になければならない、という constraint があるが、これはすべての定数式に適用されるものなのかどうか不 明である。例外は記載されてないところをみると、文言の上ではすべての定数式 に適用されると解釈するしかないが、Standard C の意図は「定数式の必要なと ころでは」ということであろう。明確にすべきである。また、他方で「符号なし 型は決して overflow しない」という規定もあり、符号なし型の定数演算で範囲 を超えた時に診断メッセージを出すべきかどうかは、ことにあいまいである(定 数式はコンパイル時に評価できるものであるので、診断メッセージを出すのが適 当と思われる)。 しかし、これはプリプロセス固有の問題ではないので、これ以上は論じません。 また、C90 では /, % 演算子による整数の割り算について、分母・分子の片方 または双方が負数の場合の結果は implementation-defined というひどい規定が ありましたが、C99 では div(), ldiv() と同様の規定となりました。 *1 C99 への各種 defect report やそれに対する response および corrigendum の draft は次の ftp site にある。これは ISO/SC22/WG14 の official ftp server となっており、少なくとも今のところは anonymous ftp できる(SC というのは steering committee の略で、WG は working group である。SC22 というのはプログラム言語の規格を審議するところで、 WG14 は C の規格を担当している)。 http://osiris.dkuug.dk/JTC1/SC22/WG14/ ☆ 2.Validation Suite 解説 ☆ [2.1] Validation Suite for Conformance of Preprocessing test-t, test-c, test-l, tool, cpp-test というディレクトリに入っている もの、およびこの cpp-test.txt そのものが、私の作った "Validation Suite for Standard C Conformance of Preprocessing" (「プリプロセスの標準C適 合性検証用ソウトウェア一式」)です。これは任意の処理系のプリプロセスの Standard C (ANSI/ISO/JIS C) 準拠度を詳細にテストするものです。Standard C で規定されているすべてのプリプロセス仕様を網羅しているつもりです。さらに、 規定外の事項に関するおまけもたくさんあります。 test-t というディレクトリには 183 本のサンプルテキストが入っています。 そのうち 30 本はヘッダファイル、145 本は細切れのサンプルテキストで、8 本 は細切れのサンプルテキストをまとめたファイルです。ヘッダファイル以外は一 部は *.cc という名前ですが、あとはすべて *.t という名前がついています。 これはコンパイルフェーズとは関係なく、プリプロセスフェーズだけをテストす るものです。したがって、必ずしもCの正しいプログラムの形にはなっていませ ん。プリプロセステスト専用のサンプルテキストと言うべきものです。 Standard C の処理系はプリプロセスとコンパイルとを圧縮して処理すること もできるので、処理系によってはプリプロセスだけを切り離してテストすること ができません。この *.t のサンプルそのものが Standard C に従っていないと も言えます。しかし、プリプロセスだけを切り離してテストできる処理系も多く あり、できるなら切り離してテストしたほうが仕様も問題点も明確になります。 この *.t のサンプルはそのためのものです。 C++ のサンプルは、処理系によっては *.c, *.t という名前では C++ のソー スとして扱わないものもあるので、*.cc という名前のものも用意しています。 その内容は対応する *.t と同じです。 サンプルテキストのファイルには名前が n_ で始まるもの(normal の意)、 i_ で始まるもの(implementation-dependent の意)、m_ で始まるもの (multibyte character の意)、e_ で始まるもの(erroneous の意)がありま す。 n_ で始まるものは、プリプロセスに関しては間違いも undefined な動作を引 き起こすものも implementation-defined な部分も含まないサンプルです。 Standard C 準拠のプリプロセッサは、これをエラーにせずに正しく処理できな ければなりません。 i_ で始まるものは、文字セットに関する implementation-defined な仕様に 依存するサンプルで、ASCII 基本文字セットを前提としています。ASCII 文字セ ットを持つ Standard C 準拠の処理系のプリプロセッサは、これをエラーにせず に正しく処理できなければなりません。 e_ で始まるものは、何らかの violation of syntax rule or constraint、す なわちエラーを含むサンプルです。Standard C 準拠のプリプロセッサは、これ を見逃すことなく正しく診断できなければなりません。 n_, i_, m_, e_ の後に数字の続くものは C90 のプリプロセス, および C90 と C99 の共通のプリプロセス仕様をテストするサンプルです。ヘッダファイル のうち、pragmas.h, ifdef15.h, ifdef31.h, ifdef63.h, long4095.h および nest9.h から nest15.h までは C99 のプリプロセス仕様をテストするサンプル で、他のヘッダファイルは C90 と C99 の共通のものです。 n_, i_, e_, u_ に std, post 以外のアルファベットの続くものは C99 およ び C++ のサンプルです。n_dslcom.t, n_ucn1.t, e_ucn.t, u_concat.t は C99 と C++98 の共通の、n_bool.t, n_cnvucn.t, n_cplus.t, e_operat.t, u_cplus. t は C++ の、その他は C99 のプリプロセス仕様をテストするサンプルです。 ?_std.t という名前のファイルは C90 の細切れファイルをまとめたものです。 ?_std99.t はその C99 版です。?_post.t, ?_post99.t という名前のファイルは オマケで、MCPP の post-Standard モードのテストのためのものです。 u_*.t という名前のファイルはオマケで、undefined behavior のテストをす る細切れのファイルです。undefs.t はそれらを1本にまとめたものです。unbal?. h はそれらで使われるヘッダファイルです。unspcs.t は unspecified behavior のテストをするもので、warns.t は以上のどれにも当てはまらないが処理系がウ ォーニングを出すのが望ましいテキストを記載したファイルです。unspcs.t, warns.t もオマケです。m_ で始まるものは、multi-byte character, wide character として数種の encoding を持つものを用意しています。できれば、多 くの encoding を正しく処理することが望まれます。m_* は規格準拠性のテスト ではなく、u_* 等と同様に品質の評価項目に属しています。 misc.t, recurs.t, trad.t はオマケ中のオマケです。この3つはプリプロセ ッサの評点の対象とはしません。misc.t は規格書やその他の書籍に載っている もの、整数型の内部表現によって結果が異なるテキスト、translation phase 5, 6 に関するテスト、拡張機能のテスト、等を集めたものです。recurs.t は特殊 な再帰的マクロのサンプルで、trad.t は古い "Reiser model cpp" のためのサ ンプルです。 test-c というディレクトリには 132 本のファイルが入っており、そのうち 26 本はヘッダファイル(24 本は test-t のものと同じ)、102 本は細切れのサ ンプルソースで、2 本は細切れのサンプルソースをまとめたファイル、他の 2 本は自動テストに使うファイルです。これらのうち 31 本はオマケのサンプルソ ースです。ヘッダファイル以外のソースはすべて *.c という名前がついていま す。これはCのプログラムの形になっています。 やはり、n_, i_, m_, e_ で始まる名前がついています。n_ で始まるものは、 Standard C で言う strictly conforming program (間違いも処理系依存部分も 持たないプログラム)です。処理系はこれらをエラーにせずに正しくコンパイル し、正しく実行できなければなりません。正しく実行できた場合はそれぞれ started success というメッセージが出ます。n_std.c に限っては、これらのメッセージは出ず、 といった終了のメッセージだけが出ます。そうでない場合は何らかの失敗のメッ セージが出ます。i_ で始まるものは ASCII を前提とした文字定数のサンプルで、 ASCII 文字セットを持つ処理系は、これを n_ で始まるものと同様に正しくコン パイルし、正しく実行できなければなりません。e_ で始まるものについては、 処理系はコンパイル(プリプロセス)時に正しく診断できなければなりません。 コンパイルあるいは実行によるテストは最も正式のテスト方法ですが、その方 法では処理系に間違いがあることはわかっても、どこに間違いがあるのかが明確 にならない場合があります。これらの *.c ファイルもプリプロセッサにだけ通 して、その結果を目で見てテストすることもできるので、処理系が許す限りそう したほうが正確な評価ができます(*.t ファイルのほうがさらに端的である)。 ?_std.c という名前のファイルは細切れファイルをまとめたものです。 u_*.c という名前のファイルはオマケで、undefined behavior のテストをす る細切れのファイルです。undefs.c はそれらを1本にまとめたものです。 unspcs.c は unspecified behavior のテストをするもので、warns.c は以上の どれにも当てはまらないが処理系がウォーニングを出すのが望ましいテキストを 記載したファイルです。unspcs.c, warns.c もオマケです。m_ で始まるサンプ ルには数種の multi-byte character encoding に対応したものがあります。 test-c ディレクトリには C99 のテストは含まれていません。まだ、C99 に十 分対応しているコンパイラ本体が見当たらないからです。C++ のテストも test- t ディレクトリにしかありません。 test-l ディレクトリに入っているのは、規定を上回る translation limits のテストをするためのサンプルです。144 本のすべてがオマケです。*.c, *.t, *.h ファイルが混ざっています。 *.h ファイルは test-t, test-c, test-l の各ディレクトリに重複して含まれ ているものがかなりあります。重複するヘッダファイルを一つのディレクトリに まとめると、例えば #include "../test-t/nest1.h" といったインクルードのしかたが必要になりますが、こうしたパスリストの形式 やファイルを探す時の方法(基準ディレクトリをどこに置くか等)がすべて implementation-defined であり、互換性が保証されていないので、この問題を 避けるために重複をいとわず各ディレクトリにヘッダファイルを置いています (そもそも「ディレクトリ」という概念さえも C Standard からは除外されてい る)。 tool というディレクトリには、自動テスト等を行うのに必要なツールが入っ ています。 cpp-test というディレクトリにあるのは、GNU C / testsuite で使うための サンプルです。test-t, test-l ディレクトリのものを testsuite 用に書き直し た版を置いています。 Standard C 準拠の要件としては、処理系が正しく動作することはもちろんで すが、それだけでなく、ドキュメントに必要な事項が正確に記載されていること が必要です。それについては [2.5] で説明します。 [2.2] テスト方法 Validation Suite でテストする時には、処理系に Standard C に近付けるた めのオプションがある場合は、それらをすべて指定します(具体例は [5.1] 参 照)。 [2.2.1] 手動テスト test-t, test-c の各ディレクトリとも、大きくまとめたファイルとかなり細 切れのファイルと、2種のサンプルがあります。プリプロセッサの Standard C 準拠度が高ければ、少なくとも n_* についてはまとめたファイルだけでテスト できますが、さほど高くない場合はこれらのファイルでは途中からプリプロセッ サが混乱に陥って、それ以降の項目のテストができなくなってしまうことがある ので、細切れのファイルも用意してあります。しかし、あまり細切れにするとフ ァイルの数がやたらに増えてテストの手間がかかるので、いい加減なところで妥 協しています。処理系によっては、この細切れのサンプルでさえも最後まで処理 できないことがあります。その場合はそのサンプルをさらに分割してテストして ください。 また、#error directive は処理系によっては処理を終了させるので、#error のテストをするサンプルはまとめたファイルに入れていません。#include のエ ラーも処理をそこで終了させることが多いので、まとめたファイルには入れてい ません。 *.t のサンプルは、プリプロセッサが独立したプログラムになっている場合、 またはコンパイラにプリプロセス後のテキストを出力するオプションがある場合 に使います。これらのファイルをプリプロセスした結果を直接、目で見て、コメ ント中に書かれている正しい結果と一致するかどうかを確かめます。プリプロセ スの結果を直接見ることができるので、処理系が許す限りこちらでテストしたほ うが正確な判断ができます。 *.c のプログラムの多くは "defs.h" というヘッダファイルを include して います。この "defs.h" には処理系の Standard C 準拠度に応じて3種の assert マクロの定義が書いてあるので、そのうちのどれか1つを囲んでいる # if 0 の 0 を 1 にします。プリプロセッサの # 演算子が実装されている処理系 では、1番目または2番目のものを使います。1番目のものは を include するだけです。2番目のものは assertion が失敗しても abort しない assert マクロです。これはもちろん正しい assert マクロではありませんが、 このテストではむしろこちらのほうが便利でしょう。3番目のものは assertion が失敗した時に単に "Assertion failed" と表示するだけのもので、何の assertion が失敗したのかがわかりません。# 演算子が実装されていない処理系 や、実装されてはいるが間違いがあってコンパイルエラーを引き起こす処理系で は、しょうがないのでこれを使います。 という標準ヘッダファイルがない古い処理系では、これを書いて おく必要があります([4.1.3] 参照)。 void という keyword の存在しない古い処理系では の最後に臨時 に #undef void #define void と書いてからテストをしてください。 Multi-byte character の処理は実行時環境によって処理系の動作仕様が変わ る場合があるので、m_* のテストには注意が必要です([3.1] 参照)。 こうしたテストには難しい問題があります。ある事項をテストしようとすると、 処理系の他の欠陥にひっかかってしまうことがあるからです。例えば が間違っている場合、それを include して #if のテストをしようとすると、 のテストをしているのか #if のテストをしているのかわからなくな ってしまいます。*.c ファイルをコンパイルし実行するテストでは、*.t ファイ ルをプリプロセスするテストよりもっとやっかいになります。最終的な結果が間 違っていれば、処理系に何らかの欠陥があることははっきりしますが、テストし ようとした事項に欠陥があるのかどうかは、必ずしも明らかではありません。 この Validation Suite ではターゲットとする事項に的を絞れるようにいろい ろ工夫したつもりです。しかし、Validation Suite 自体は portable でなけれ ばならないという制約があり、また、ある事項をテストするためには他の言語仕 様が正しく実装されていることを前提とせざるをえません。したがって、この 「前提」として使われるプリプロセスの事項は、その事項をターゲットとしたテ スト項目以外のところでも暗黙のうちにテストされることになります。次に述べ る「配点」についても、そうした暗黙の配点があることに留意してください。ま た、処理系があるサンプルの処理に失敗した場合、本当にそのサンプルがターゲ ットとするテスト項目で失敗したのか、それとも他の要因で失敗したのかは、他 のテストとも併せて見ないと判断できないことがあります。 各テスト項目については、それぞれ配点を決めています。採点の基準も書いて あります。Standard C にはサブセットというものはないので、全項目が規定に 合致していなければ厳密には Standard C 準拠とは言えませんが、現実にはそう いう処理系はなかなかないので、処理系の評価には Standard C 準拠度という物 差しを使わざるをえません。そして、項目の重要度には大きな差があるので、合 格項目の数だけを数えるわけにはゆかず、重要度に応じた配点をせざるをえませ ん。 しかし、もちろんこの配点には客観的な基準はありません。この Validation Suite の配点は私が勝手に決めたもので、大した根拠はありません。それでも、 処理系の規格合致度を客観的に評価するための目安にはなるでしょう。 n_*, i_*, e_*, d_* は規格準拠度に関するテストで、これらは原則として 2 点単位で配点します。規格外のテストと品質の評価では、q_* は 2 点単位で、 他は 1 点単位で配点します。診断メッセージを出すべきところでは、出るには 出たが見当外れなものである場合は、点はやらないことにします。まったく間違 いではないもののかなりピントのずれた診断メッセージの場合は、半分だけ点を やることがあります。また、正しいプログラムを正しく処理したうえで診断メッ セージを出すことは処理系の自由ですが、間違った診断メッセージが出る場合は 減点の対象とします。 [2.2.2] cpp_test による自動テスト tool ディレクトリにある cpp_test.c というプログラムをコンパイルして test-c ディレクトリで動かすと、C90 の n_*.c, i_*.c のテストを自動的に行 うことができます。ただし、これは○×をつけるだけで、詳細を知ることはでき ません。e_*.? 等のテストも含まれていません。プリプロセッサの C90 準拠度 について簡単に見当をつけるためのものです。C99 についてのテストも含まれて いません。現在はまだコンパイラの大半が C99 には対応していないからです。* 1 cpp_test.c をコンパイルする時は、__MSDOS__ というマクロが事前定義され ない MS-DOS 上の処理系の場合は -D__MSDOS__ というオプションを付けてコン パイルしてください。*2 実行する前に、m_33_*.c, m_34_*.c のうちの適当な encoding のものをそれ ぞれ i_33.c, i_34.c にコピーしておいてください。*3 cpp_test の使い方は Visual C++ .net 2003 を例にとると、次の通りです。 cpp_test VC2003 "cl -Za -Tc -Fe%s %s.c" "del %s.exe" < n_i_.lst 第二引数以下はそれぞれ " と " で囲む必要があります(shell が ", " をは がしてしまう場合は、第二引数から最後の引数までの全体を ' と ' でさらに囲 む等の対策が必要)。%s は n_*, i_* 等、サンプルプログラムの名前から .c を取り除いたもので置き換えられます。 第一引数: 処理系の名前を指定します。これは8バイト以内で、'.' を含ま ないものでなければなりません(MS-DOS のつごうに合わせてある)。この 名前に .out, .err, .sum をつけた名前のファイルが作成されます。 第二引数: コンパイルするためのコマンドを書きます。 第三引数以下: 用のすんだファイルを削除するコマンドを書きます。これは 複数あってかまいません。 n_i_.lst は test-c ディレクトリにあります。n_*.c, i_*.c の各ファイル名 から .c を取り除いたもののリストが書かれています。 処理系によっては、どれかのソースの処理で暴走を始めるものもあります。そ の場合は、n_i_.lst のそのソースの名前を例えば none といった存在しないフ ァイル名に書き替えて、再実行します。 こうして cpp_test を実行すると、n_*.c, i_*.c が順次コンパイルされ、実 行されてゆきます。n_*.err, i_*.err というファイルに、サンプルプログラム の stderr への出力が記録されます。そして、VC2003.sum にその採点結果が縦 1列に書き込まれます。と言っても、次の3種類の採点しかありません。 *: 合格 o: コンパイルはできたが、実行結果が不合格 -: コンパイルできなかった VC2003.out に cpp_test の呼び出したコマンドラインが記録され、処理系が stdout に出したメッセージがあればそれもそこに記録されます。VC2003.err に は、処理系が stderr に出したメッセージがあれば、それが記録されます。これ らを見ることによって、もう少し何かを知ることができるでしょう。 さらに、次のようにします。 paste -d'\0' side_cpp *.sum > cpp_test.sum こうすると、各処理系のテスト結果である *.sum ファイルが横に連結されて 1つの表が作成され、cpp_test.sum に書き込まれます。side_cpp はテスト項目 のタイトルを書いた表側部分で、test-c ディレクトリにあります。 私がこうして作った cpp_test.sum を doc ディレトクリに置いてあります。 なお、[5] には手動による詳細テストの結果が書いてあります。そちらでは cpp_test.sum より多くのプリプロセッサをテストしています。それらのプリプ ロセッサの中にはどの処理系のコンパイラドライバにも対応していないものがあ り、そうしたプリプロセッサについては cpp_test による自動テストはできませ ん。 *1 この cpp_test.c は "Plum-Hall Validation Sampler" の runtest.c と summtest.c というプログラムを下敷きにして書いたものである。 *2 cpp_test.c は Borland C / bcc32 でコンパイルすると期待通りの動作を しない。cpp_test は stdout, stderr をリダイレクトしたうえで system() を呼び出しているが、bcc32 では標準入出力が子孫のプロセスに継承されな いようである。Visual C, LCC-Win32 でコンパイルしたものは問題なく動く。 *3 m_36_*.c は 0x5c ('\\') の値のバイトを持つ encoding のテストである が、これらの encoding を通常は使わないシステムもあるので、ここでは除 外する。 [2.2.3] GNU C / testsuite による自動テスト [2.2.3.1] TestSuite とは GNU C には testsuite というものがあります。GNU C のソースをコンパイル した後で、make -k check とすると、この testsuite のサンプルが次々とチェ ックされ、結果が報告されます。 私の検証セットは V.1.3 からは、GNU C の testsuite としても使える形に書 き直した edition が追加されました。これを testsuite に入れておくと、make check で自動的にチェックされます。[2.2.2] の cpp_test というツールでは n_*, i_* という名前のサンプルしかテストできませんが、testsuite では e_*, w_*, u_* 等の診断メッセージを要するサンプルも自動的にテストできます。こ の版は GNU C 2.9x / cpp, GNU C 3.x / cpp (cc1, cc1plus), MCPP のテストを することができます。 ここでは、GNU C / testsuite での検証セットの使い方を説明します。 検証セットの cpp-test ディレクトリは test-t, test-l ディレクトリを GNU C / testsuite に対応して書き直したもので、cpp-test の中に test-t, test-l の各ディレクトリがあります。 ただし、m_*, u_1_7_* は multi-byte character の各種 encoding に対応し たものですが、GNU C 3.3 までのバージョンでは環境変数の設定を encoding に 応じて変えることが必要で、コンパイル時の環境を変えることが testsuite で はできないので、cpp-test には含めていません。* GNU C / cpp の仕様の変更や testsuite の仕様の変更がこれまでにいろいろ あり、これからもあると予想されますが、それによっては検証セットの一部修正 が必要になります。ことに診断メッセージが追加されたり変更されたりした場合 は、testcase にそれを取り込む必要があります。しかし、今のところは GNU C のバージョンがひどく古くない限り、大幅な修正は必要ないと思われます。cpp- test の testcases は GNU C 2.95.3, 2.95.4, 3.2R, 3.3.2, 3.4.3 の各 cpp (cpp0, cc1, cc1plus) および MCPP で動作を確かめてあります。 testsuite では実行時オプションを処理系によって変えることはできません。 複数の規格が事実上併存している現在の状況では std= というオプションで規格 のバージョンを明示する必要がありますが、このオプションは GNU C の古いバ ージョンには存在しません。したがって、私の testsuite の適用範囲は GNU C 2.9x 以降と MCPP V.2.3 以降ということになります。 なお、testsuite は egcs-1.1.2 (GNU C 2.91.66) までは存在していましたが、 GNU C 2.95.3 には存在していません。この時期は testsuite の一部に著作権の 問題があったために、公開が保留されていたものです。しかし、GNU C 3.0 では 復活したようです。 Testsuite はサンプルプログラム中に書かれた次のような形式のコメントを解 釈して実行されます。これはコメントなので、他の処理系のテストには影響しま せん。 /* { dg-do preprocess } */ /* { dg-error "out of range" } */ dg-error とか dg-warning というコメントの書かれたサンプルでは、診断メ ッセージがテストされます。複数の処理系のテストをすることも、それぞれの処 理系の診断メッセージを '|' (OR) をはさんで並べて書くことで、可能になりま す。 これを実行するのは DejaGnu というツールで、直接には runtest という shell-script です。DejaGnu の設定は *.exp という名前のいくつかのファイル に書かれています。*.exp は expect というツールのための script です。そし て、この expect は Tcl というコマンド言語で書かれたプログラムなのです。 したがって、testsuite を使うには、その testsuite に応じて、これらの多 くのツールの適切なバージョンのものがそろっていなければなりません。これは 私の検証セットを使う場合でも、使わない場合と同じです。 * 実際には環境変数を設定しても GNU C は正しく動作しない。 [2.2.3.2] TestSuite へのインストールとテスト GNU C / testsuite で私の検証セットを使うには、次のようにします。 まず、 cpp-test ディレクトリを GNU C の testsuite の適当なディレクトリ にコピーします。 cpp-test ディレクトリは、test-t, test-l の各ディレクトリの必要なファイ ルを書き直し、cpp-test.exp というテスト用の設定ファイルを加えたたもので す。*.t という名前のファイルは suffix を .c と変えてあります。C++ のファ イルは *.C という名前にしてあります。 大半のサンプルはプリプロセッサだけをテストするものですが、2つだけは DejaGnu, Tcl の問題でそれができないので、compile & run するためのサンプ ルになっています(*_run.c という名前のもの)。この2つのサンプルには { dg-options "-ansi -no-integrated-cpp" } という行があります。-no- integrated-cpp というのは GNU C 3 のためのオプションです。GNU C 2 にはこ のオプションはないので、GNU C 2 でテストする場合はこのオプションをはずさ なければなりません。そのために、この2つの testcase については *_run.c. gcc2, *_run.c.gcc3 という2種のファイルを用意しています。その適当なほう を *_run.c にリンクして使ってください。 以下では、私のところの GNU C 3.2 を例にとって説明します。GNU C 3.2 の ソースが /usr/local/gcc-3.2-src に置かれているとします。また、GNU C のコ ンパイルは /usr/local/gcc-3.2-objs で行うとします。 cp -r cpp-test /usr/local/gcc-3.2-src/gcc/testsuite/gcc.dg これで、gcc.dg ディレクトリに cpp-test 以下のファイルがコピーされます。 こうしておくと、/usr/local/gcc-3.2-objs で make bootstrap として GNU C のソースをコンパイルした後で、 make -k check とすると、cpp-test の testcases を含めたすべての testsuite がテストされ ます。 また、cpp-test だけ使ってテストするには、/usr/local/gcc-3.2-objs/gcc ディレクトリで次のようにします。 make check-gcc RUNTESTFLAGS=cpp-test.exp Testsuite の log は ./testsuite ディレクトリの gcc.log, gcc.sum に記録 されます。 make check する時は、GNU C のソースの INSTALL/test.html に説明があるよ うに、DEJAGNULIBS, TCL_LIBRARY という環境変数の設定が必要です。たとえば、 私のところの VineLinux では /etc/profile で次のようにしています。 TCL_LIBRARY=/usr/lib/tcl8.0jp DEJAGNULIBS=/usr/share/dejagnu export TCL_LIBRARY DEJAGNULIBS また、GNU C 3 では環境変数 LANG, LC_ALL を C として英語環境にしなけれ ばなりません。 GNU C のコンパイル時に make check で使われるのは、すでにインストールさ れている gcc, cc1 等ではなく、gcc ディレクトリに生成された xgcc, cc1, cc1plus, cpp0 等であることに注意してください。 cpp-test によるテストは次のようにしてもできます。 runtest --tool gcc --srcdir /usr/local/gcc-3.2-src/gcc/testsuite \ cpp-test.exp この場合は、実行するディレクトリは任意です。ログはカレントディレクトリに 出力されます。テストされるのは、すでにインストールされている gcc, cc1, cpp0 等です。cpp-test で testsuite が必要なのは、GNU C 用の各種の設定フ ァイル (config.*, *.exp) がそこにあるからです。 この runtest --tool gcc で指定する 'gcc' という名前はこのとおりでなけ ればなりません。もし実際に使うコンパイラがたとえば cc, gcc-3.4.3 等と gcc 以外の名前の場合は、シンボリックリンクを作って gcc という名前で呼び 出せるようにしておきます。 なお、cpp-test にはプリプロセッサがウォーニングを出すのが望ましいと考 えられるケースについてのウォーニングの testcases も多く含まれています。 GNU C のプリプロセッサはそれらの半分以下しか PASS しませんが、通らないか らといって、動作が間違っているとか、プリプロセッサが正しくコンパイルされ ていないとかということではありません。これは正しいか間違っているかの問題 ではなく、プリプロセッサの「品質」の問題なのです。 [2.2.3.3] MCPP の自動テスト この cpp-test では標準モードの MCPP のテストもできるようになっています。 したがって、GNU C のプリプロセスを MCPP に置き換えて、gcc ディレクトリで make check-gcc RUNTESTFLAGS=cpp-test.exp とすると、MCPP の自動チェックがされます。任意のディレクトリで runtest を 直接呼び出して実行することもできます。 runtest --tool gcc --srcdir /usr/local/gcc-3.2-src/gcc/testsuite \ cpp-test.exp MCPP は GNU C 3 で実行した場合、cpp-test の testcases が1件を除いてす べてが PASS するはずです。GNU C 2 で実行した場合はもう1件、通らないもの がありますが、それは gcc が -D__cplusplus=1 というオプションを付けて MCPP を呼び出すからであり、MCPP のせいではありません。 プリプロセスを MCPP に置き換える方法については、manual.txt [3.9.5], [3. 9.7] を見てください。Testsuite を適用する時は、MCPP の起動に -23j という オプションを付けるように設定する必要があります。-2 は digraph、-3 は trigraph を有効にするオプションです。-j は診断メッセージの出力にソース行 等の情報を付加しないためのオプションです。他のオプションは設定しないでく ださい。また、testsuite でテストできるのは標準モードの MCPP だけで、他の モードの MCPP のテストはできません。 以上は GNU C の make の後で行う方法ですが、GNU C / testsuite がインス トールされていて実行できる状態になっていれば、MCPP 自身の configure と make で MCPP の自動テストをすることができます。この場合は、必要な設定を すべて make check が自動的にやってくれるので、最も簡単です。この方法につ いては、MCPP の INSTALL を見てください。 [2.2.3.4] TestSuite と検証セット GNU C には testsuite が古くからありましたが、プリプロセスに関しては V. 2.9x まではごくわずかのサンプルしかありませんでした。いかにプリプロセス が軽視されていたかがわかります。このプリプロセスの testcases が V.3.x で はケタ違いに増えました。プリプロセッサのソースとドキュメントが一新され、 up-to-date なものになったことに伴うもので、プリプロセスが重視されるよう になったことがわかります。 しかし、この testcases は依然としてかなり片寄ったものになっています。 その原因は、testsuite の次のような性格に由来するものだと思われます。 1、ユーザから寄せられる bug report の集積であること。すなわち、実際に 発見されたバグを修正し再発を防ぐためのものが中心である。 2、そこに、開発者が新しい機能を実装した時に、そのデバッグのために書い たものが追加されている。 これはオープンソースならではのデバッグ方法で、GNU C が世界の多くの優れ たプログラマに使われてきたことで可能となったものです。しかし、この方法は 同時に、testcases のランダム性と片寄りをもたらしてきたと考えられます。 また、これらの testcases には GNU C でしか通用しないサンプルが多く、他 の処理系には適用できません。それどころか、GNU C 3 の testcases には GNU C 2 / cpp にさえも適用できないものが多く含まれています。プリプロセス出力 の spacing の違いや診断メッセージの違いがあるためです。 これに対して私の検証セットは、初めは私のプリプロセッサのデバッグのため に私が一人で書いたものですが、途中で考え方を変えて、プリプロセスの全仕様 をテストするように書き直されました。多くのサンプルが全体としてシステマテ ィックに構成されています。こうしたシステマティックな testcases が GNU C / testsuite に追加されることは、少なくない意味を持つと思われます。 また、私の testsuite 版検証セットは GNU C 2.9x / cpp, GNU C 3.x / cpp, MCPP という3つのプリプロセッサのテストができるように書かれています。す なわち、DejaGnu, Tcl の正規表現の機能を活用すれば、処理系によるプリプロ セス出力の spacing の差異や診断メッセージの差異を吸収することができるの です。* Testsuite 化した検証セットをこの3つのプリプロセッサに適用した結果を以 下に載せておきます(2005/03 のテスト)。 GNU C 3.4.3 でプリプロセッサを標準モードの MCPP V.2.5 に置き換えて実行 した場合はこうなります。 === gcc Summary === # of expected passes 266 # of unexpected failures 1 # of expected failures 2 gcc version 3.4.3 1つだけ FAIL があるのは、C++98 での universal-character-name <=> multi-byte character の変換を実装していないことによるものです。 GNU C 3.2 / cpp0 ではこうなります。 === gcc Summary === # of expected passes 217 # of unexpected failures 52 # of unexpected successes 1 # of expected failures 1 /usr/local/gcc-3.2/bin/gcc version 3.2 FAIL の多くは、ウォーニングを出してほしいところで出してくれないという ものです。 GNU C 3.4.3 / cc1 もほとんど変わりません。 === gcc Summary === # of expected passes 215 # of unexpected failures 54 # of unexpected successes 1 # of expected failures 1 /usr/local/bin/gcc version 3.4.3 GNU C 2.95.3 / cpp0 ではこうなります。 === gcc Summary === # of expected passes 181 # of unexpected failures 87 # of unexpected successes 1 # of expected failures 1 gcc version 2.95.3 20010315 (release) GNU C 3 / cpp よりもさらにウォーニングが少なく、ピントの外れた診断メッ セージもいくつかあります。C99, C++98 の新しい仕様も半分が未実装です。 GNU C のバージョンによって件数が少し違うのは、1つの testcase 中で複数 の FAIL が発生することがあるからです。 * このために、私の testcases の dg script はやたらに \ や記号の多い unreadable なものになっている。 DejaGnu, Tcl の正規表現の処理にはかなりの癖や欠陥があり、使うには種 々の工夫が必要であるが、工夫すれば複数の処理系について一通りの自動テ ストが可能であることがわかった。 ただし、現状では、それらの処理系の実行時オプションのうち testcases で使われるオプションが共通であることが条件である。 [2.3] Violation of syntax rule or constraint と診断メッセージ Standard C の処理系は正しいソースを正しく処理しなければならないのはも ちろんですが、間違ったソースに対しては診断メッセージを出さなければなりま せん。また、Standard C には、動作仕様が処理系にまかされている部分や、ど ういう動作をするか規定されていない部分もあります。これらのことをまとめて 示すと、次のようになります。*1 1.正しいプログラムと正しいデータで、どの処理系でも同じ処理結果になる もの。 2.正しいプログラムとデータであっても、その処理方法は規定されないもの。 これはドキュメントに記載しなくても良い。これを unspecified behavior(未 規定の動作)と呼ぶ。 3.正しいプログラムとデータであっても、その処理は処理系にまかされてい るもの。この仕様は各処理系がドキュメントに記載しなければならない。これを implementation-defined behavior(処理系定義の動作)と呼ぶ。 4.間違ったあるいは移植性のないプログラムまたはデータであるが、その処 理については何も規定されないもの。処理系はこれに診断メッセージを出しても 良いし、出さなくても良い。何らかの正しいプログラムとしての処理をしても良 い。これを undefined behavior(未定義の動作)と呼ぶ。 5.間違ったプログラムまたはデータで、それに対して処理系が diagnostic message(診断メッセージ)を出さなければならないもの。これには violation of syntax rule(構文規則違反)と violation of constraint (制約違反)と がある。*2 これらのうち1のプログラムとデータだけのものを strictly conforming program (規格厳密合致プログラム)と呼びます(2,3も、処理系や場合によ って結果に違いの出ないものであれば、含まれていても良いと解釈される)。 1,2,3のプログラムとデータだけのものを conforming program (規格合 致プログラム)と呼びます。 診断メッセージの出し方は implementation-defined です。何らかの violation of syntax rule or constraint を含む translation unit 1つにつ いて1つ以上の何らかの診断メッセージを出さなければならないとされています。 Violation of syntax rule or constraint を含まないプログラムに対して、診 断メッセージを出すことは処理系の自由です。ただし、もちろん strictly conforming program や、その処理系の implementation-defined, unspecified の仕様に合致した conforming program は最後まで正しく処理できなければなり ません。 このドキュメントでは、violation of syntax rule or constraint を「エラ ー」と呼ぶことにします。 この Validation Suite の e_* ファイルには、複数のエラーを含むものがた くさんあります。以下の採点では、処理系が1つのエラーについて1つ以上の診 断メッセージを出すことを期待しています。しかし、いくつエラーがあっても1 つの translation unit について1つの診断メッセージ("violation of syntax rule or constraint" といった)しか出さない処理系もあるかもしれません。ま た、エラーがあるとその後の処理が混乱する処理系もあるかもしれません。この 種の問題は処理系の「品質」の問題ですが、規格準拠度の問題ではありません。 必要な場合はサンプルをバラして再テストしてください。品質の問題は [3] で 別にとりあげます。 *1 ANSI C 1.6 (C90 3) Definitions of Terms 用語の定義及び規約 ANSI C 1.7 (C90 4) Compliance 規格合致性 ANSI C 2.1.1.3 (C90 5.1.1.3) Diagnostics 診断メッセージ C99 3 Terms, definitions, and symbols C99 4 Conformance C99 5.1.1.3 Diagnostics *2 C++98 では C90, C99 とは用語がかなり違っているが、主旨は特に違わな い。 [2.4] 詳細 以下に、各テスト項目について1つずつ解説します。これは Standard C プリ プロセスそのものの解説ともなるものです。しかし、K&R 1st. と共通の仕様に ついては、あらためて解説はしません。項目の番号は *.t ファイルと *.c ファ イルとで共通です。 [2.4.1] Trigraphs [n.1.1] 9つの trigraph sequences Cの基本文字セットには ISO 646:1983 の Invariant Code Set に含まれない 文字が9つあるので、これらは次の3文字の sequence でもソース中に書くこと ができるようになりました。これは C90 で新設された規定です。* ??= # ??( [ ??/ \ ??) ] ??' ^ ??< { ??! | ??> } ??- ~ これらの9つの trigraph sequence は translation phase 1 で対応する文字 に置き換えられます。9つの文字をキーボードから入力することのできるシステ ムでは、もちろん trigraph を使う必要はありません。しかし、そういうシステ ムでも Standard C に合致するプリプロセスは trigraph の変換ができる必要が あります。 配点 : 6。9つとも正しく処理できれば 6 点、正しく処理できない trigraph が1つあるごとに 2 点減点し、0 点を下限とする。 * ANSI C 2.2.1.1 (C90 5.2.1.1) Trigraph sequences 3文字表記 ANSI C 2.1.1.2 (C90 5.1.1.2) Translation phases 翻訳フェーズ C99 5.2.1.1 Trigraph sequences C99 5.1.1.2 Translation phases [n.1.2] コントロール行中の trigraph sequence Trigraphs の変換は translation phase 3 での tokenization や phase 4 で のコントロール行の処理に先立って行われるので、もちろんコントロール行の中 でもどこでも trigraphs を書くことができます。 配点 : 2。 [n.1.3] Trigraphs は9つしか存在しない Trigraphs は上記の9つしかないので、それ以外の ?? で始まる sequence は 他の文字に変換されることはなく、?? がスキップされることもありません。 Trigraph とそうでない ? を含む sequence が混在している場合も、プリプロセ スは正しく処理できる必要があります。 配点 : 2。 [2.4.2] による行接続 行末に \ があってその直後に改行が続いているばあいは、この の sequence は translation phase 2 で無条件に削除されます。そ の結果、2つの行が接続されます。規格書では、ソースファイル上の行を physical line(物理行)、 が(あれば)削除されて接続 された後の行を logical line(論理行)と呼んで区別しています。Translation phase 3 の処理はこの論理行を対象として行われます。* K&R 1st. では #define 行と文字列定数は で次のソー ス行に続けることができるとされていましたが、その以外の場合については触れ られていません。実際の処理系ではまちまちですが、コントロール行は #define に限らず接続できるものが多かったようです。 * ANSI C 2.1.1.2 (C90 5.1.1.2) Translation phases 翻訳フェーズ C99 5.1.1.2 Translation phases [n.2.1] #define 行のパラメータリストと置換リストとの間 #define 行の接続は K&R 1st. でも認められていたものであり、ほとんどの処 理系で可能です。 配点 : 4。すなわち、正しく処理できれば 4 点、できなければ 0 点。 [n.2.2] #define 行のパラメータリストの中 しかし、#define 行であってもパラメータリストの中のようなあまり普通では ないところに があると、正しく処理できない処理系もあ ります。 配点 : 2。 [n.2.3] 文字列リテラルの中 文字列リテラルの中の も K&R 1st. 以来のものです。 配点 : 2。 [n.2.4] 識別子の中 Standard C では、識別子の中であろうとどこであろうと、 は無条件で削除されなければなりません。 配点 : 2。 [n.2.5] Trigraph による は \ だけでなく、trigraph による ??/ もあります。ソース上 の ??/ は translation phase 1 で \ に変換されていますから、phase 2 では 当然 \ そのものです。 配点 : 2。 [2.4.3] コメント Translation phase 3 では論理行が pp-token と white spaces に分解されま す。その際、コメントは1個の space に変換されます。*1 ここで処理系は、連続する white spaces(コメントを含む)を1個の space に変換するかもしれません。ただし、どちらにしても は変換せず、 そのまま残します。それは、次の phase 4 での preprocessing directive の処 理がこの「行」を対象とするからです。 コメントが行をまたがっている場合は、コメントによる事実上の行接続が行わ れます。 *1 ANSI C 2.1.1.2 (C90 5.1.1.2) Translation phases 翻訳フェーズ C99 5.1.1.2 Translation phases [n.3.1] One space に変換 いわゆる Reiser 型の古い cpp ではコメントは、cpp の内部でだけ token separator として機能し、出力までの間には削除されていました。そのことを利 用して、トークンの連結にコメントを使う方法がありましたが、この仕様は K&R 1st. からも外れたものであり、Standard C では明確に否定されました。 Standard C ではトークン連結には ## 演算子を使います。 配点 : 6。 [n.dslcom] // コメント K&R 1st. から C90 に至るまで、コメントは /* で始まり */ で終わるもので した。*1 しかし、C99 では C++ の // もコメントとして扱うことになりました。*2 配点 : 4。 C90 ではこれはコメントではなく単なる // という pp-token sequence とし て処理すべきものであり、この Validation Suite でも V.1.2 まではそれをテ ストしていた。しかし、C99 以前から // をコメントとして処理する処理系が一 般的で、それを期待しているソースも多く、C99 で公認されたことでもあるので、 このテストは V.1.3 からははずした。 MCPP でも、V.2.2 までは C90 モードでは // をコメントとして扱っていなか ったが、V.2.3 からは C90 モードでもこれをコメントとして処理した上でウォ ーニングを出すようにした。 *1 ANSI C 3.1.9 (C90 6.1.9) Comments コメント *2 C99 6.4.9 Comments [n.3.3] コメントは pp-directive の処理に先立って処理 # で始まる preprocessing directive は「行」を対象としますが、この「行」 はソースの物理行とは限りません。 によって接続された 論理行であることもあれば、物理・論理行をまたぐコメントによって複数の物理・ 論理行にまたがる「行」であることもあります。これは translation phase 1 から 4 の順序を考えてみれば、当然のことです。 配点 : 4。 [n.3.4] コメントと とコメントの双方によっていくつかの物理行にまたが る pp-directive 行も存在します。Translation phase を正しく実装していない プリプロセッサでは、これを正しく処理することができません。 配点 : 2。 [2.4.4] 特殊なトークン (digraphs) と文字 (UCN) C90 / Amendment 1 (1994) では、いくつかの operator, punctuator につい て digraph という代替 spelling が追加されました。*1 C99 では UCN (universal character sequence) という文字表記が追加されま した。*2 e.4.? ではトークンエラーを取り上げます。 *1 Amendment 1 / 3.1 Operators, 3.2 Punctuators (ISO 9899 / 6.1.5, 6. 1.6 への追加) C99 6.4.6 Punctuators *2 C99 6.4.3 Universal character names [n.4.1] Preprocessing directive 行中の digraph spelling Digraph は token (pp-token) として扱われます。%: は # の別 spelling で す。もちろん、preprocessing directive 行の先頭 pp-token としても、文字列 化演算子としても使えます。 配点 : 6。 [n.4.2] Digraph spelling の文字列化 Digraph は trigraph と違って token (pp-token) なので、文字列化に際して は変換されずにそのままの spelling で文字列化されます(無意味な仕様である)。 配点 : 2。 [n.ucn1] UCN の認識 1 UCN は文字列リテラル、文字定数、識別子の中で認識されます。 配点 : 8。文字列リテラル中の UCN がプリプロセスをそのまま通過すれば 4 点、文字定数、identifier の中の UCN が正しく処理されれば各 2 点。UCN で あることを認識せず黙ってそのまま出力するものは不可。 [n.ucn2] UCN の認識 2 UCN は pp-number 中でも使えます。しかし、プリプロセスの終りまでに number-token からは消滅していなければなりません。これは C99 だけの規定で、 C++ にはありません。 配点 : 2。 [e.ucn] UCN の誤り UCN は \U で始まるものは8ケタ、\u で始まるものは4ケタの16進数でな ければなりません。 UCN は [0..9F], [D800..DFFF] の範囲にあってはなりません。ただし、24($), 40(@), 60(`) は有効です。 配点 : 4。4つのサンプルのそれぞれについて、正しく診断できれば各 1 点。 [e.4.3] 空の文字定数 Cのトークンの形をしていない sequence でもたいていは pp-token として認 められます。したがって、プリプロセスの tokenization でエラーとなる場合は、 あまりありません。 ただし、このほかに undefined behavior となる場合がいくつかあります([3. 2] 参照)。 カラの文字定数はプリプロセスの #if 行でもコンパイルでも、violation of syntax rule です。* 配点 : 2。 * ANSI C 3.1.3.4 (C90 6.1.3.4) Character constants 文字定数 -- Syntax 構文規則 C99 6.4.4.4 Character constants -- Syntax [2.4.5] Preprocessing directive line 中の space と tab Space, tab, vertical-tab, form-feed, carriage-return, new-line は white space として一括されます。文字列リテラル・文字定数・ヘッダ名・コメ ントの中にない white space は通常は token separator としての意味しかもっ ていませんが、translation phase 4 まで残った new line は特別なもので、pp- directive の区切りとなります。そして、pp-directive line 中では使える white space に若干の制限があります。 [n.5.1] # の前後の space と tab Standard C では、Preprocessing directive line の最初の pp-token である # の前後の space, tab は、あってもなくても同じ結果になることが保証されて います(*)。K&R 1st. ではこれが明確ではなく、実際にも # の前後の space, tab を認めない処理系がありました。 その行のそれ以降の space, tab は、K&R 1st. でも Standard C と同様に、 単なる token separator として認められていたと解釈されます。 しかし、pp-directive line に space, tab 以外の white space があった場 合は undefined です([3.2] の [u.1.6] 参照)。 配点 : 6。 * ANSI C 3.8 (C90 6.8) Preprocessing directives 前処理指令 -- Description 補足規定, Constraints 制約 C99 6.10 Preprocesssing directives -- Description, Constraints [2.4.6] #include #include は K&R 1st. 以来の最も基本的な pp-directive です。しかし、 Standard C のこの directive の規定には undefined な部分と implementation- defined な部分とが多くなっています(*)。その理由は次の点にあります。 1. この directive の引数である header-name という pp-token はOSのフ ァイルシステムに依存するものであり、標準化しがたい。 2. ファイルをさがす「標準」の場所は処理系に依存する。 3. ", " で囲まれた文字列リテラル類似の形式の header-name でさえも、\ は escape 文字ではない等、文字列リテラルとは別の pp-token であり、さらに <, > で囲まれた header-name にいたっては最も変則的な pp-token である。 n.6.* では、次のような最も基本的なテストは、項目を分けて行うことはしま せん。これは他のテスト項目に前提として含まれており、これが処理できないの では多くのテストが実施できないので、論外です。 #include #include "header.h" * ANSI C 3.8.2 (C90 6.8.2) Source file inclusion ソースファイル取り込 み C99 6.10.2 Source file inclusion [n.6.1] 2つの形式による標準ヘッダの include Header-name には2つの形式があります。その違いは、<, > で囲まれた形式 では処理系定義の特定の場所(複数でも良い)からその header をさがし、", " で囲まれた形式ではそのソースファイルをまず処理系定義の方法でさがして、そ れが失敗した時は <, > で囲まれた形式と同様の処理をする、ということにすぎ ません。したがって、", " で囲まれた形式で標準ヘッダを include することも できます。この点は K&R 1st. でも同様でした。 なお、Standard C では、同じ標準ヘッダを複数回 include してもかまわない ことになっています。しかし、それはどちらにしてもプリプロセッサの問題では なく、標準ヘッダの書き方の問題です([4.1.1] 参照)。 配点 : 10。2つのサンプルの片方しか成功しないのは 4 点。 [n.6.2] マクロによる header-name・その1 K&R 1st. では #include 行にマクロが使えるとはされていませんでしたが、 Standard C ではマクロが公認されました。#include の引数が2つのどちらの形 式とも一致しない場合は、そこに含まれるマクロが展開されます。その結果は2 つの形式のどちらかに一致する必要があります。 この規定には微妙なところもあります。例えば、次のようなソースはどう扱わ れるべきでしょうか。 #define MACRO header.h> #include とすべきでしょうか。それとも、マクロ展開の前に < に対応する > がないとし てエラーとすべきでしょうか。 規格はこういうところまで意識して書かれているとは考えられません。したが って、<, > は ", " と同様の quotation delimiter として扱うのがすなおだと 思われます。もちろん、先にマクロを展開するのも、規格違反とは言えませんが。 この Validation Suite には、こうした規格の穴をつつくテストは含まれていま せん。 配点 : 6。 [n.6.3] マクロによる header-name・その2 つまらないことですが、マクロは必ずしも1つである必要はありません。 配点 : 2。 [2.4.7] #line #line という pp-directive はユーザが使うことは普通はないものですが、他 の何かのツールで(Cとは限らない)ソースを pre-preprocess した場合などに、 元のソースのファイル名とその時々の行番号を伝えるために使われるもののよう です。K&R 1st. 以来あるところを見ると、伝統的にそれなりの用途があるので しょう。 また、プリプロセッサの出力にも一般に #line またはその変形が、ファイル 名と行番号情報をコンパイラ本体に伝えるために使われます。しかし、これは規 定されていることではありません。 #line で指定されたファイル名と行番号は事前定義マクロ __FILE__, __LINE__ の値となります(__LINE__ はさらに物理行1行ごとにインクリメント されてゆく)。* * ANSI C 3.8.4 (C90 6.8.4) Line control 行制御 C99 6.10.4 Line control ANSI C 3.8.8 (C90 6.8.8) Predefined macro names あらかじめ定義され たマクロ名 C99 6.10.8 Predefined macro names [n.7.1] 行番号とファイル名の指定 Line number と filename を指定した #line は K&R 1st. でも Standard C でも同じです。 なお、ファイル名は文字列リテラルですが、これは #include の header-name とは違ったトークンであり、厳密に考えると \ の扱い等で微妙な問題を含んで います。しかし、正しいソースであれば問題は生じないでしょう(#line の filename に の形のものがないのは幸いである)。 配点 : 6。 [n.7.2] ファイル名指定は無くても良い Filename の引数はオプションであり、なくてもかまいません。これも K&R 1st. と同じです(line number の指定のないのは undefined)。 配点 : 4。 [n.7.3] マクロによる行番号とファイル名の指定 K&R 1st. では #line の引数にマクロが使えるとはされていませんでしたが、 Standard C では使えることが保証されました。 配点 : 4。 [n.line] 行番号の範囲 #line ディレクティブの line number の範囲は C90 では [1..32767] でした が、C99 では [1..2147483647] に拡大されました。 配点 : 2。 [e.7.4] ファイル名がワイド文字列リテラル Filename の引数は文字列リテラルでなければならず、つまらないことですが、 ワイド文字列リテラルでは violation of constraint となります(それ以外の pp-token ではなぜか undefined。アンバランスな規定である)。 配点 : 2。 [2.4.8] #error #error は Standard C で新設された directive です。プリプロセス時に、引 数をその一部として含むエラーメッセージを表示します。古い処理系では # assert 等の directive を持つものもありましたが、標準的なものはありません でした。 なお、#error では処理を終了すべきかどうかは規定されていませんが、 Rationale によると、規格ではそこまで要求できないので規定しなかっただけで、 中止するのが ANSI C 委員会の意図であるとのことです。* * ANSI C 3.8.5 (C90 6.8.5) Error directive 表示指令 ANSI C Rationale 3.8.5 C99 6.10.5 Error directive C99 Rationale 6.10.5 [n.8.1] マクロは展開されない #error 行中のマクロは展開されません。コントロール行でマクロが展開され るのは、#if (#elif), #include, #line だけです。* 配点 : 8。マクロを展開して処理するものは 2 点。処理を終了するかどうか は問わない。 * ANSI C 3.8 (C90 6.8) Preprocessing directives 前処理指令 -- Semantics 意味規則 C99 6.10 Preprocessing directives -- Semantics [n.8.2] メッセージは無くても良い つまらないことですが、#error 行の引数はオプションであり、なくてもかま いません。 配点 : 2。 [2.4.9] #pragma, _Pragma() #pragma も Standard C で新設されたものです。処理系独自の拡張 directive はすべて #pragma sub-directive で実現することになっています。*1 *1 ANSI C 3.8.6 (C90 6.8.6) Pragma directive プラグマ指令 C99 6.10.6 Pragma directive [n.9.1] 認識できない #pragma もエラーにはならない したがって、処理系ごとに認識できる #pragma sub-directive が異なります。 認識できない #pragma がいちいちエラーになったのでは portable なプログラ ムが書けないので、処理系が認識できない #pragma は無視することになってい ます。プリプロセスでは自分の認識できるプリプロセスに関する #pragma だけ を処理して、他の #pragma はすべてそのままコンパイラ本体に渡すことになり ます。 配点 : 10。#pragma をエラーにしてはいけないが、ウォーニングを出すこと はかまわない。プリプロセスはエラーにしないのにコンパイラ本体がエラーにす る場合というのはないであろうが、もしあってもプリプロセスの間違いではない ので 10 点を与える。しかし、プリプロセッサが独立していない処理系でこの区 別がつかない場合は、便宜上、0 点とする。 [n.pragma] _Pragma() operater C99 では、#pragma と同じ効果を持ち、#pragma と違ってマクロ定義中にも書 くことのできる _Pragma() operator が追加されました。 また、pragma に続く pp-token が STDC であれば規定された標準の機能を持 ち、この場合はマクロ展開が禁止されるが、他の場合はマクロ展開の対象とする かどうかは implementation-defined とされています。 配点 : 6。 [e.pragma] _Pragma() の引数は文字列リテラル _Pragma() operator の引数は文字列リテラルでなければなりません。 配点 : 2。 [2.4.10] #if, #elif, #else, #endif #if, #else, #endif は K&R 1st. 以来あるものです。* #if が使えない処理系では、n.10.1, n.11.*, n.12.*, n.13.*, e.12.*, e.14. * がどれも処理できないばかりでなく、他の多くのテストも失敗することになり ます。 * ANSI C 3.8.1 (C90 6.8.1) Conditional inclusion 条件付き取り込み C99 6.10.1 Conditional inclusion [n.10.1] #elif が使える Standard C では #elif が追加されました。これによって、#if を何重にもネ ストする読みにくい書き方が避けられるようになりました。 #if 式中にはマクロを使うこともできます。マクロとして定義されていない identifier は 0 と評価されます。 なお、規格書では、#if からそれに対応する #endif までを #if section、そ の中の #if (#ifdef, #ifndef), #elif, #else, #endif で区切られたブロック を #if group と呼んでいます。#if, #elif 行の式を #if 式と呼びます。 配点 : 10。 [n.10.2] スキップされる #if group での pp-token 処理 スキップされる #if group では、#if group の対応関係をトレースするため に #if, #ifdef, #ifndef, #elif, #else, #endif を調べる以外は preprocessing directive は処理されず、マクロも展開されません。 しかし、tokenization は行われます。それは第一に、Cのソースは初めから 終わりまでコメントと pp-token の sequence だからであり、第二に、#if 等の 対応関係を調べるためには少なくともコメントの処理が必要で、/* 等がコメン ト記号であることを確かめるためにはそれが文字列リテラルや文字定数の中にあ るのではないことを確かめなければならないからです。 配点 : 6。 [2.4.11] #if defined Standard C では #if 式に defined という演算子が新設されました。これは #ifdef, #ifndef を #if (#elif) に統合するもので、これによって、#ifdef が 何重にもネストされた読みにくい書き方が避けられるようになりました。 [n.11.1] #if defined defined 演算子の operand は識別子ですが、それを (, ) で囲む書き方と囲 まない書き方の双方が認められています。どちらも意味は同じで、operand がマ クロとして定義されていれば 1、そうでなければ 0 と評価されます。 配点 : 8。2つの #if section の片方しか処理できないものは 2 点。 [n.11.2] defined は単項演算子 defined は演算子の1つであり、1 か 0 のどちらかの値を与えるので、その 結果をさらに他の式と演算することができます。 配点 : 2。 [2.4.12] #if 式の型 #if 式については K&R 1st. では漠然と定数式とされているだけで、その型が はっきりしていませんでした。C90 では #if 式は整数定数式であり、int は long と、unsigned int は unsigned long と同じ内部表現を持っているかのよ うに扱うことが明確にされました。すなわち、#if 式はその中の副式も含めてす べて long および unsigned long で評価されます。言い換えれば、定数トーク ンにはすべて L または UL の接尾子が付いているのと同じように扱われます。* 1 さらに、C99 では #if 式の型はそれぞれの処理系の最大の整数型とされまし た。この型は という標準ヘッダで intmax_t, uintmax_t という型 名に typedef されます。C99 では long long は必須とされているので、#if 式 の型は long long またはそれ以上のサイズということになります。long long / unsigned long long の定数を表記するには LL (ll) / ULL (ull) という接尾子 を使います。*2 long long / unsigned long long の値を printf() で表示するには、%ll と いう length modifier と適当な conversion specifier を使います(%lld, % llu, %llx 等)。intmax_t, uintmax_t の値を表示する length modifier は %j です(%jd, %ju, %jx 等)。*3 *1 ANSI C 3.8.1 (C90 6.8.1) Conditional inclusion 条件付き取り込み -- Semantics 意味規則 *2 C99 6.10.1 Conditional inclusion -- Semantics *3 C99 7.19.6.1 The fprintf function -- Description [n.12.1] long 型の #if 式 C90 の #if 式では long 型の定数式が評価できなければなりません。 sizeof (long) > sizeof (int) の処理系の中には #if 式を int や unsigned int でしか評価できないものがあり、中には long を黙って truncate して評価 するものもあります。2つ目のサンプルは後者をチェックするためのものです。 配点 : 6。 [n.12.2] unsigned long 型の #if 式 C90 の #if 式では unsigned long 型の定数式が評価できなければなりません。 配点 : 4。 [n.12.3] 8進数 #if 式では8進定数も評価できなければなりません。K&R 1st. でも同様です が、long の最大値を越える定数が K&R 1st. では long の負の値に評価された のに対して、C90 では unsigned long に評価されるところが違っています。*1 配点 : 4。8進数を認識するが負数に評価したりオーバーフローしたりする ものは 2 点、8進数を認識しないものは 0 点。 * ANSI C 3.1.3.2 (C90 6.1.3.2) Integer constants 整数定数 C99 6.4.4.1 Integer constants [n.12.4] 16進数 #if 式では16進定数も評価できなければなりません。K&R 1st. でも同様で すが、long の最大値を越える定数が K&R 1st. では long の負の値に評価され たのに対して、C90 では unsigned long に評価されるところが違っています。 配点 : 4。16進数を認識するが負数に評価したりオーバーフローしたりす るものは 2 点、16進数を認識しないものは 0 点。 [n.12.5] 接尾子 L, l #if 式では接尾子 L, l の付いた定数トークンも評価できなければなりません。 K&R 1st. でも long の最大値を越えない定数については同じです。プリプロセ スではこの接尾子はあってもなくても評価は同じです。 配点 : 2。 [n.12.6] 接尾子 U, u #if 式では接尾子 U, u の付いた定数トークンも評価できなければなりません。 これは K&R 1st. にはなく、C90 で公認された記法です。 配点 : 6。 [n.12.7] 負数 #if 式では負の数も扱えなければなりません。K&R 1st. 以来の仕様です。 配点 : 4。 [e.12.8] 定数トークンの値が表現できる範囲にない C90 では、#if 式に現れる整数定数トークンの値が long または unsigned long で表現できる範囲にない場合は violation of constraint、平たく言えば エラーとなります。これは #if 式について直接規定されていることではありま せんが、定数式一般についての規定があり、#if 式も例外ではないと考えられま す。* なお、文字定数の overflow については、e.32.5, e.33.2, e.35.2 でテスト します。 配点 : 2。 * ANSI C 3.4 (C90 6.4) Constant expressions 定数式 -- Constraints 制 約 C99 6.6 Constant expressions -- Constraints [n.llong] long long / unsigned long long の評価 C99 の #if 式では少なくとも long long / unsigned long long 型の定数式 が評価できなければなりません。 long long の最大値を越える定数は unsigned long long に評価されます。 C99 では LL, ll, ULL, ull という接尾子も追加されました。* 配点 : 10。 5つのサンプルについて、正しく処理できればそれぞれ 2 点。 * C99 6.4.4.1 Integer constants C99 6.6 Constant expressions -- Constraints [e.intmax] 演算の結果が int_max_t の範囲外 C99 の #if 式では、intmax_t, uintmax_t の範囲を越える定数および定数式 は violation of constraint となります。 配点 : 2。 がない場合はマクロを適当に書いてもかまわない。 [2.4.13] #if 式の演算 #if 式は整数定数式の一種です。一般の整数定数式と比べると次のような違い があるだけです。 1.C90 では int は long、unsigned int は unsigned long と同じ内部表現 を持つかのように評価される。C99 では int, long は intmax_t、unsigned int, unsigned long は uintmax_t と同じ内部表現を持つかのように評価される。 2.defined 演算子が使える。 3.マクロを展開した後に残った identifier はすべて 0 と評価される。*1 4.プリプロセスでは keyword が存在しないので、keyword と同名の iden- tifier は単なる identifier として扱われる。したがって、キャストや sizeof は使えない。 関数呼び出し、コンマ演算子は、一般の整数定数式でも使えません。定数式は 変数ではないので、代入、インクリメント、デクリメント、配列も使えません。 n.13 では一般の整数定数式と共通の評価ルールについてテストします。この うち n.13.5 は K&R 1st. とは変わったところで、n.13.6 は pre-Standard の #if 式ではまちまちだったところで、n.13.13, n.13.14 は K&R 1st. では明確 でなかったものですが、他は K&R 1st. 以来まったく変わっていません。*2 なお、#if 式で int の値しか評価できない処理系についてもこのルールのテ ストができるように、n.13 ではすべて小さい値しか使っていません。また、演 算子 defined, >= は n.13 に出てきませんが、他のどこかに出てきています。 *1 C++ Standard では true, false は特別扱いされ、それぞれ 1, 0 と評価 される。これはマクロではなく keyword であるが、プリプロセスでは boolean literal として扱われることになっている。 *2 ANSI C 3.3 (C90 6.3) Expressions 式 ANSI C 3.4 (C90 6.4) Constant expressions 定数式 C99 6.5 Expressions C99 6.6 Constant expressions [n.13.1] <<, >> ビットシフト演算は少なくとも正数に関しては、面倒な問題は何もありません。 配点 : 2。 [n.13.2] ^, |, & 正の整数はその型の範囲におさまっている限り、同じ値のビットパターンは CPU や処理系のいかんにかかわらず同じなので、^, |, & という一見 CPU の仕 様に依存しそうな演算も、その範囲ではどの処理系でもまったく同じ結果になり ます。 配点 : 2。 [n.13.3] ||, && これを処理できない処理系はないでしょう。 配点 : 4。 [n.13.4] ? : これも処理できない処理系はないでしょう(と思われたが、実際にはなくもな い)。 配点 : 2。 [n.13.5] <<, >> では通常の算術変換は行われない 多くの2項演算子では両辺の型をそろえるため usual arithmetic conversion (通常の算術変換)が行われます。K&R 1st. ではシフト演算子でもこれが行わ れることになっていましたが、Standard C では行わないことになりました。右 辺の値は常に小さい正数であること、2の補数以外の内部表現では、負数が正数 に変換されたのではビットパターンも変わってしまい、何をやっているのかわか らなくなってしまうことを考えると、これは妥当な規定でしょう。* 配点 : 2。 * ANSI C 3.3.7 (C90 6.3.7) Bitwise shift operators ビット単位のシフト 演算子 -- Semantics 意味規則 C99 6.5.7 Bitwise shift operators -- Semantics 多くの2項演算子に関しては「両辺が算術型であれば、それらに対して通常 の算術変換が行われる」といちいち書いてあるが、<<, >> に関してはこの 記載がない。C89 Rationale 3.3.7 (C99 Rationale 6.5.7) にこれについて の解説がある。 [n.13.6] 通常の算術変換による負数の正数への変換 2項演算子 *, /, %, +, -, <, >, <=, >=, ==, !=, &, ^, |, に関しては、 両辺の型をそろえるため、両辺の operand に対して通常の算術変換が行われま す。3項演算子 ? : の第2、第3 operand についても同様です。したがって、 片方が符号なし型であると、他方も符号なし型に変換され、負数が正数になって しまいます。 なお、一般の整数定数式では通常の算術変換の前に、各 operand が int より 短い整数型であった場合はそれに対して integer promotion (汎整数拡張)が 行われますが、#if 式ではすべての operand が同じサイズの型として扱われる ので、integral promotion は発生しません。 配点 : 6。3つのテストを1つ正しく処理できるごとに 2 点。 この評価規則は K&R 1st. でも同じなのであるが、このサンプルは 0U という 定数トークンが U 接尾子を使っているので、K&R 1st. の処理系では処理できな い。その場合は、この 0U を符号なし型に評価される十分大きな値で、しかし - 1 を符号なし型に変換した時の値よりも大きくない値で書き替えて、テストをや り直す必要がある。Pre-ANSI の処理系では符号なし型の #if 式を書く方法のな いものもある。 [n.13.7] ||, &&, ? : での評価の打ち切り ||, && 演算子では評価順序が決まっており、左辺の評価で結果が確定すれば 右辺は評価されません。? : では第一 operand の評価結果によって第二か第三 のどちらか一方が評価され、他方は評価されません。* したがって、評価されない項にはたとえば 0 による除算があってもエラーに はなりません。 配点 : 6。5つのサンプルのうち失敗が1つあるごとに 2 点を減点。2つ以 下しか成功しないのは 0 点。間違った診断メッセージが出るのは 2 点減点。 * ? : 演算子ではしかし、第二 operand と第三 operand との間の通常の算 術変換は行われることになっている。評価しないのに変換するとは妙なこと である。ことに #if 式で使われる整数定数トークンは値を評価しないと型 が決まらないので、型(と言っても符号付きか符号無しかだけであるが)を 判定するためには、値を評価せざるをえない。しかし、0 による除算はして はいけないのである。やっかいなことである。 [n.13.8] 単項演算子 -, +, !, ~ のグループ化 n.13.8 から n.13.12 までは #if 式中の副式のグループ化のテストです。副 式は演算子の優先順位と結合規則に従ってグループ化されます。一般の整数定数 式では優先順位の前に構文で決まる部分がありますが、#if 式はグルーピングの (, ) 以外には構文が問題となるところはありません。n.13.8 から n.13.10 ま では結合規則のテストで、n.13.8 は単項演算子 -, +, !, ~ の結合規則のテス トです。単項演算子はいずれも右から左へ結合されます。 配点 : 2。 [n.13.9] ? : のグループ化 条件演算子 ? : は右から左へ結合されます。 配点 : 2。 [n.13.10] <<, >> のグループ化 2項演算子はいずれも左から右へ結合されます。n.13.10 では <<, >> のテス トをします。 配点 : 2。 [n.13.11] 優先順位の異なる演算子のグループ化・その1 単項演算子 -, +, ! と2項演算子 *, /, >> という、優先順位も結合規則も 異なる演算子の含まれる式のテストをします。 配点 : 2。 [n.13.12] 優先順位の異なる演算子のグループ化・その2 単項演算子 -, +, ~, ! と2項演算子 -, *, %, >>, &, | ^, ==, !=、それに 3項演算子 ? : を含むさらに複雑な式のグループ化のテストをします。 配点 : 2。 [n.13.13] 演算子に展開されるマクロ #if 式にはマクロを含むことができます。このマクロは通常は整数定数に展開 されるものですが、それについては n.10.1, n.12.1, n_12.2, n.13.7 のテスト に含まれているので、ここではあらためてテストしません。 演算子に展開されるマクロというのは尋常なものではありませんが、やはり原 則として演算子として扱われるべきでしょう。ISO C 1990 / Amendment 1 では という標準ヘッダが規定されましたが、これはいくつかの演算子を マクロで定義するものです (*)。&, |, !, ^, ~ といった文字を使わなくてもソ ースを書くことができるように、という趣旨のようです。プリプロセスは #if ではこれらのマクロを展開しながら、演算子として扱うことが求められます。 しかし、他方で #if 式中のマクロが defined に展開されると undefined と いう規定があります。defined は identifier と紛らわしいので別扱いになった ものでしょう([u.1.19] 参照)。 配点 : 4。 * C++ Standard では、なぜかこれらの identifier 様演算子はマクロではな く token である。 [n.13.14] 0個のトークンに展開されるマクロ 0個のトークンに展開されるマクロを含む #if 式も尋常なものではありませ んが、これはやはりそのマクロをとり除いた(展開した)上で評価されるべきで しょう。 配点 : 2。 [2.4.14] #if 式のエラー e.14.1 から e.14.10 までは #if 式での violation of syntax rule と violation of constraint のテストです。処理系はこれらのどれか1つを含むす べてのソースに対して、診断メッセージを出す必要があります。* * ANSI C 3.8.1 (C90 6.8.1) Conditional inclusion 条件付き取り込み ANSI C 3.4 (C90 6.4) Constant expressions 定数式 C99 6.10.1 Conditional inclusion C99 6.6 Constant expressions [e.14.1] 文字列リテラル #if 式は整数定数式であり、ポインタは使えないので、文字列リテラルは使え ません。 配点 : 2。 [e.14.2] 演算子 =, ++, --, . 等 #if 式は定数式であるので、副作用を持つ演算子や変数は使えません。A --B は A - -B とは違って violation of constraint です。 配点 : 4。4つのサンプルのうち正しく診断できないものがあれば1つあれ ば 2 点、2つ以上あれば 0 点。 [e.14.3] 完結しない式 2項演算子の operand の片方がなかったり、かっこの対応がとれなかったり するものは、もちろん violation of syntax rule です。 配点 : 2。 [e.14.4] #if defined のカッコが片方だけある #if 行の defined という演算子の引数は (, ) で囲んでも囲まなくてもかま いませんが、カッコの片方だけあるのは violation of constraint です。 配点 : 2。 [e.14.5] 式がない #if だけで式がないのはもちろん violation of syntax rule です。 配点 : 2。 [e.14.6] マクロを展開すると式がなくなる マクロとして定義されていない identifier は 0 と評価されますが、マクロ を展開すると何もなくなってしまう #if 行の引数はもちろん violation of syntax rule です。 配点 : 2。 [e.14.7] Keyword は認識されない・sizeof sizeof という pp-token はプリプロセスでは単なる identifier として扱わ れ、#if 式では、マクロとして定義されていなければ 0 と評価されます。int という pp-token も同様です。したがって、sizeof (int) は 0 (0) となり、 violation of syntax rule となります。 配点 : 2。 [e.14.8] Keyword は認識されない・型名 e.14.7 と同様に、(int)0x8000 は (0)0x8000 となり、やはり violation of syntax rule となります。 配点 : 2。 [e.14.9] 0 による除算 この e.14.9 と次の e.14.10 については、診断メッセージを出すべきかどう か、いくつかの解釈の余地があり、規定があいまいです。規格書には次のような 規定があります。 ANSI C 3.4 (C90 6.4) Constant expressions 定数式 -- Constraint 制約 C99 6.6 Constant expressions -- Constraint Each constant expression shall evaluate to a constant that is in the range of representable values for its type. 定数式を評価した結果は、その型で表現可能な値の範囲内にある定数でな ければならない。 この規定の適用範囲が明らかではありませんが、少なくとも定数式の必要なと ころには適用されることは自明です。#if 式はもちろん定数式でなければなりま せん。しかし、他方で次のような規定もあります。 ANSI C 3.3.5 (C90 6.3.5) Multiplicative operators 乗除演算子 -- Semantics 意味規則 C99 6.5.5 Multiplicative operators -- Semantics if the value of the second operand is zero, the behavior is undefined. 第2オペランドの値が0の場合、動作は未定義とする。 ANSI C 3.1.2.5 (C90 6.1.2.5) Types 型 C99 6.2.5 Types A computation involving unsigned operands can never overflow, 符号無しオペランドを含む計算は、決してオーバーフローしない。 0 による除算と符号なし演算では、どちらの規定を適用すべきでしょうか? どちらの解釈もありうるように思えます。 しかし、ここではこう解釈することにします−−定数式の要求されるところで は 0 による除算も含めて、その型の範囲におさまらない結果となる場合は原則 として診断メッセージを出さなければならない−−。定数式でこうした結果にな る場合というのはプログラムの間違いしか考えられず、定数式は実行時ではなく コンパイル時に評価されるものなので、これに診断メッセージを出すのが妥当と 思われるからです。また、0 による除算だけ例外扱いするのも不自然だからです。 ただし、符号無し演算の結果が範囲外となる場合については規定のあいまいさが 二重になるので、ここには含めず、undefined と解釈することにします。 ISO 9899:1990 / Corrigendum 1 では、「Violation of syntax rule or constraint があれば、たとえ他のところで undefined or implementation- defined と明示的に規定されていても、処理系は診断メッセージを出さなければ ならない」という規定が追加されました。これは C99 にも引き継がれています。 * 配点 : 2。 * C99 5.1.1.3 Diagnostics [e.14.10] 演算の結果が表現できる範囲にない C90 では、#if 式の値は long / unsigned long で表現できる範囲になければ なりません。 配点 : 4。4つのテストをすべて正しく診断できれば 4 点、3つか2つ正し く診断できれば 2 点、1つ以下しか正しく診断できなければ 0 点。 [2.4.15] #ifdef, #ifndef n.15 は #ifdef, #ifndef に関するテストです。これは K&R 1st. でも Standard C でもまったく同じです。e.15 はその violation of syntax rule の テストです。* * ANSI C 3.8 (C90 6.8) Preprocessing directives 前処理指令 -- Syntax 構文規則 ANSI C 3.8.1 (C90 6.8.1) Conditional inclusion 条件付き取り込み C99 6.10 Preprocessing directives -- Syntax C99 6.10.1 Conditional inclusion [n.15.1] #ifdef によるマクロのテスト 配点 : 6。 [n.15.2] #ifndef によるマクロのテスト 配点 : 6。 [e.15.3] 識別子でない引数 #ifdef, #ifndef 行の引数は identifier でなければなりません。 配点 : 2。 [e.15.4] 余計な引数 #ifdef, #ifndef 行の引数に identifier 以外の余計なトークンがあってはい けません。 配点 : 2。 [e.15.5] 引数がない 何も引数がないのも、もちろん violation of syntax rule です。 配点 : 2。 [2.4.16] #else, #endif のエラー 次は #else, #endif の violation of syntax rule のテストです。この syntax も K&R 1st. から変わっていません(しかし、violation of syntax rule or constraint に対して診断メッセージを出さなければならないというの は、Standard C で初めて規定されたことである)。* * ANSI C 3.8 (C90 6.8) Preprocessing directives 前処理指令 -- Syntax 構文規則 C99 6.10 Preprocessing directives -- Syntax [e.16.1] #else に余計なトークンがある #else の行には他のどんなトークンもあってはいけません。 配点 : 2。 [e.16.2] #endif に余計なトークンがある #endif の行には他のどんなトークンがあってもいけません。 #if MACRO #else ! MACRO #endif MACRO ではなく、 #if MACRO #else /* ! MACRO */ #endif /* MACRO */ と書きましょう。 配点 : 2。 [2.4.17] #if, #elif, #else, #endif の対応関係のエラー 次は #if (#ifdef, #ifndef), #elif, #else, #endif の対応に関する violation of syntax rule のテストです。この syntax もほぼ K&R 1st. 以来 同じですが、#elif は Standard C で新しく加えられたものです。またこれらの 対応はソースファイル単位で成り立っていなければならないという点は、K&R 1st. では明確ではなかったところです。 * ANSI C 3.8 (C90 6.8) Preprocessing directives 前処理指令 -- Syntax 構文規則 C99 6.10 Preprocessing directives -- Syntax [e.17.1] #if のない #endif 先行する #if がないのに #endif が出てくるのは、もちろん violation of syntax rule です。 配点 : 2。 [e.17.2] #if のない #else 対応する #if のない #else ももちろん間違いです。 配点 : 2。 [e.17.3] #else の後にまた #else #else の後にまた #else が出てくるのも、もちろんいけません。 配点 : 2。 [e.17.4] #else の後に #elif #else の後に #elif が出てきてもいけません。 配点 : 2。 [e.17.5] インクルードされたファイル中の #if のない #endif #if, #else, #endif 等はソースファイル(preprocessing file)単位で対応 がとれていなければなりません。インクルードされたファイルがあたかも初めか ら元ファイルの中にあったかのように扱うと初めて対応がとれる、というのでは ダメです。 配点 : 2。 [e.17.6] インクルードされたファイル中の #endif のない #if 配点 : 2。 [e.17.7] #endif のない #if #endif を書き忘れることは実際にもよく起こりますが、処理系はそれに対し て診断メッセージを出さなければなりません。 配点 : 2。 [2.4.18] #define #define の syntax は K&R 1st. に対して C90 では #, ## 演算子が追加され ましたが、あとは変わっていません。*1 C99 ではさらに可変引数マクロが追加されました([1.8] 参照)。*2 #define ができないのではCプリプロセッサとは言えませんが、その場合は n. 18.*, e.18.* を合わせて 60 点の減点となるほか、他のテストでもマクロが使 われているので、さらに減点されることになります。 *1 ANSI C 3.8 (C90 6.8) Preprocessing directives 前処理指令 -- Syntax 構文規則 ANSI C 3.8.3 (C90 6.8.3) Macro replacement マクロ置換え *2 C99 6.10 Preprocessing directives -- Syntax C99 6.10.3 Macro replacement [n.18.1] Object-like マクロの定義 #define 行の最初のトークンはマクロ名ですが、その直後に white spaces が あると、第2のトークンが ( であっても、それは置換リストの開始とみなされ、 function-like マクロの定義とはみなされません。また、マクロ名の後に何もト ークンがなければ、そのマクロは0個のトークンに定義されます。 配点 : 30。2つのマクロのうちの1つしか正しく定義できなければ 10 点。 [n.18.2] Function-like マクロの定義 マクロ名の直後に white spaces をはさまずに ( があると、それは function- like マクロのパラメータリストの開始とみなされます。この規定は K&R 1st. 以来のもので、white spaces の有無に左右される character-oriented なプリ プロセスの痕跡が残っていますが、いまとなってはどうしようもありません。 配点 : 20。 [n.18.3] 文字列リテラル中は置換の対象にならない いわゆる "Reiser" model のプリプロセッサでは、置換リスト中の文字列リテ ラルまたは文字定数の中にパラメータと同じ部分があると、その部分がマクロ展 開によって引数に置き換えられました。しかし、これは Standard C はもちろん、 K&R 1st. でも認められていなかったものです。この置換は character-oriented なプリプロセスの特徴をよく示す仕様ですが、token-oriented なプリプロセス では論外です。 配点 : 10。 [n.vargs] 可変個引数マクロ C99 では可変個引数マクロが導入されました。 配点 : 10。 [e.18.4] 名前がない #define 行の最初のトークンはもちろん identifier でなければなりません。 配点 : 2。 [e.18.5] 引数がない #define 行にトークンが1つもないのは violation of syntax rule です。 配点 : 2。 [e.18.6] 空のパラメータ 空のパラメータも violation of syntax rule です。* 配点 : 2。 * ANSI C 3.5.4 (C90 6.5.4) Declarators 宣言子 -- Syntax 構文規則 C99 6.7.5 Declarators -- Syntax [e.18.7] パラメータ名の重複 1つのマクロ定義のパラメータリストに同じパラメータ名が複数あるのは violation of constraint です。* 配点 : 2。 * ANSI C 3.8.3 (C90 6.8.3) Macro replacement マクロ置換え -- Constraints 制約 C99 6.10.3 Macro replacement -- Constraints [e.18.8] パラメータが識別子ではない マクロ定義のパラメータは identifier でなければなりません。* 配点 : 2。 * C99 では ... というパラメータが追加された。置換リスト中の __VA_ARGS__ はそれに対応した特殊なパラメータ名である。 [e.18.9] マクロ名と置換リストの特殊な組み合わせ Standard C では identifier 中の文字として $ は認めていませんが、これを 認める有力な処理系も伝統的に存在しています。このサンプルはそうした処理系 でコンパイルされるソースに見られる種類のものです。この例は Standard C で は $ が1文字で1つの pp-token と解釈されるので、マクロ名は THIS で $ 以 降が object-like マクロの置換リストとなり、THIS$AND$THAT という名前の function-like マクロというプログラムの意図とはまったく違った結果になりま す。 ISO 9899:1990 の Corrigendum 1 では、こうした例に関して例外的規定が追 加されました。この例に対しては Standard C は診断メッセージを出さなければ ならないのです。*1 C99 ではそれに代わって、一般に object-like macro の定義では、マクロ名 と置換リストの間に white spaces がなければならないとされました。*2 配点 : 2。 *1 Corrigendum 1 による C90 6.8 の Constraints への追加。 しかし、C++ Standard ではこの規定は消えている。 *2 C99 6.10.3 Macro replacement -- Constraints [e.vargs1] 置換リストではないところに __VA_ARGS__ C99 の可変引数マクロでは、マクロ定義のパラメータ・リスト中の ... に対 応するパラメータとして置換リスト中では __VA_ARGS__ が使われます。この identifier はそれ以外のところに使ってはいけません。 配点 : 2。2つのサンプルを正しく診断できれば 2 点。1つだけでは 0 点。 [2.4.19] マクロの再定義 マクロの再定義については K&R 1st. では何も触れられておらず、実装もまち まちでした。Standard C では、元の定義と同じ再定義は許されるが、異なる定 義は認めないことになりました。マクロは事実上、再定義されないことになりま す(#undef でいったん取り消さない限り)。*1, *2 *1 ANSI C 3.8.3 (C90 6.8.3) Macro replacement マクロ置換え -- Constraints 制約 C99 6.10.3 Macro replacement -- Constraints *2 しかし、ウォーニングを出した上で再定義を認める処理系が多い。MCPP も既存の処理系との互換性のため、V.2.4 からはそうした。 [n.19.1] 置換リストの前後の white spaces の違い White spaces の数だけが違っている再定義は許されます。 配点 : 4。 [n.19.2] パラメータリスト中の white spaces の違いと 行をまたぐ white spaces の違い White spaces には、 sequence やコメントによってソ ース行をまたぐものも含まれます。 配点 : 4。 [e.19.3] 置換リストのトークン列が違う 置換リストのトークン列が異なる再定義は violation of constraint です。 配点 : 4。 [e.19.4] 置換リスト中の white space の有無が違う 置換リスト中の white space の有無が異なる再定義は violation of constraint です。ここには character-oriented なプリプロセスの尻尾が残っ ています。 配点 : 4。 [e.19.5] パラメータの使い方が違う パラメータの使い方の違う再定義は実質的にも異なる定義なので、violation of constraint です。 配点 : 4。 [e.19.6] パラメータ名が違う パラメータ名が違うだけの実質的に同じ再定義も violation of constraint となります。過剰な constraint だと思われます。 配点 : 2。 [e.19.7] Function-like, object-like マクロの名前の共用 マクロ名は一つの名前空間に属するので、function-like マクロと object- like マクロとで同じ名前を使うことはできません。 配点 : 2。 [2.4.20] Keyword と同名のマクロ プリプロセスでは keyword が存在しないので、keyword と同名の identifier もマクロとして定義し、展開することができます。* * ANSI C 3.1 (C90 6.1) Lexical elements 字句要素 -- Syntax 構文規則 C99 6.4 Lexical elements -- Syntax C89 Rationale 3.8.3 (C99 Rationale 6.10.3) Macro replacement [n.20.1] 名前はすべて展開の対象となる 配点 : 6。 [2.4.21] Pp-token の分離を要するマクロ展開 ソースファイルの pp-token への分解は translation phase 3 で行われます。 そして、そのあとで複数の pp-token が1つの pp-token に連結される場合とい うのは、## 演算子を使って定義されたマクロの展開で連結される場合、# 演算 子を使って定義されたマクロの展開で文字列化される場合、および隣接する文字 列リテラルが連結される場合しか規定されていません。したがって、複数の pp- token が暗黙のうちに連結されることは、あってはならないことだと解釈されま す。これは token-oriented なプリプロセスの原則からすると、当然のことです。 * * ANSI C 2.1.1.2 (C90 5.1.1.2) Translation phases 翻訳フェーズ ANSI C 3.8.3 (C90 6.8.3) Macro replacement マクロ置換え C99 5.1.1.2 Translation phases C99 6.10.3 Macro replacement [n.21.1] Pp-token が暗黙のうちに連結されることはない プリプロセスが独立したプログラムで行われる場合は、このサンプルの出力の 3つの - が3つの別々の pp-token であることがコンパイラ本体にわかるよう に、これらを何らかの token separator で分離して渡す必要があります。 配点 : 4。 [n.21.2] マクロ引数中のマクロの分離 マクロの引数中にマクロがあった場合も、その展開結果が置換リスト中の前後 の pp-token とくっついてしまってはいけません。 配点 : 2。 [2.4.22] Pp-number 中のマクロ類似 sequence Preprocessing-number は Standard C で初めて規定されたものです。整数定 数トークンと浮動小数点定数トークンを合わせたものよりも範囲が広く、iden- tifier 類似の部分を含むこともあります。プリプロセスでの tokenization を 単純にするために規定されたものですが、マクロ類似部分を持つ pp-number が ある場合、この単純な tokenization をその通りにやらないと、間違いが起こり ます。* * ANSI C 3.1.8 (C90 6.1.8) Preprocessing numbers 前処理数 C99 6.4.8 Preprocessing numbers [n.22.1] Pp-number 中のマクロ類似 sequence・その1 12E+EXP という sequence は1つの pp-number なので、たとえ EXP というマ クロが定義されていても、展開されることはありません。 配点 : 4。 [n.22.2] Pp-number 中のマクロ類似 sequence・その2 Pp-number は digit または . で始まります。 配点 : 2。 [n.22.3] Pp-number の外のマクロは展開される C90 では、+ または - は E または e にすぐ続く場合だけ pp-number の中に 現れることができます。12+EXP は 12E+EXP と違い、12 + EXP という3つの pp- token に分解されます。これらはそれぞれ pp-number, operator, identifier です。EXP がマクロであれば、展開されます。 配点 : 2。 [n.ppnum] [Pp][+-] の sequence C99 では、浮動小数点数を16進で表記するために、pp-number 中に P また は p に + または - が続く sequence が追加されました。 なお、printf() で浮動小数点数を16進で表示するには %a, %A という conversion specifier を使います。* 配点 : 4。 * C99 7.19.6.1 The fprintf function -- Description [2.4.23] ## 演算子を使ったマクロ ## は Standard C で新しく作られた演算子で、#define 行の置換リスト中で だけ使われます。## の前後の pp-token はマクロ展開時に連結されて1つの pp- token となります。## の前後の pp-token がパラメータの場合は、マクロ展開 時に実引数で置き換えられてから連結されます。* * ANSI C 3.8.3.3 (C90 6.8.3.3) The ## operator ##演算子 C99 6.10.3.3 The ## operator [n.23.1] トークンの連結 ## 演算子を使った最も単純な function-like マクロの例です。 配点 : 6。 [n.23.2] Pp-number の生成 ## 演算子の operand はマクロ展開されないので、この例の xglue() のよう にもう1つの一見意味がなさそうなマクロをかぶせて使うことが、しばしば行わ れます。これは引数中のマクロを展開してから連結するためです。このサンプル のマクロ呼び出しで生成された 12e+2 という sequence は有効な pp-number で す。 配点 : 2。 [e.23.3] ## の前後にトークンがない・object-like マクロ 置換リスト中の ## 演算子の前後には必ず何かの pp-token がなければなりま せん。これは object-like マクロの例です。Object-like マクロで ## を使う ことは無意味ですが、それだけではエラーではありません。 配点 : 2。 [e.23.4] ## の前後にトークンがない・function-like マクロ これは function-like マクロの定義で置換リスト中の ## 演算子の前後に pp- token のない例です。 配点 : 2。 [2.4.24] # 演算子を使ったマクロ # 演算子は Standard C で導入されたものです。Function-like マクロを定義 する #define 行の置換リストでだけ使われます。# 演算子の operand はパラメ ータで、そのマクロの展開時に対応する実引数が文字列リテラルに変換されます。 * * ANSI C 3.8.3.2 (C90 6.8.3.2) The # operator #演算子 C99 6.10.3.2 The # operator [n.24.1] 引数の文字列化 # 演算子の operand に対応する引数は両端を " と " で囲まれて文字列化さ れます。 配点 : 6。"a + b" とトークン間に space が挿入されるものは 2 点。 [n.24.2] 引数トークン間の white spaces の扱い # 演算子の operand に対応する引数が複数の pp-token の sequence で成り 立っている場合は、それらの pp-token 間の white spaces は a space に変換 されてから文字列化され、white spaces がない場合は space は挿入されません。 すなわち、white spaces の数には影響されないものの、その有無によって結果 が違ってきます(ここに character-oriented なプリプロセスの尻尾が残ってい る)。引数の前後の white spaces は削除されます。 配点 : 4。 [n.24.3] \ の挿入 # 演算子の operand に対応する引数の中に文字列リテラルまたは文字定数が ある場合は、その中にある \, " および文字列リテラルを囲む " の文字の直前 に \ が挿入されます。これは文字列リテラルや文字定数をそのままの形で表示 するための文字列リテラルを書く方法と同じです。 配点 : 6。 [n.24.4] を含むマクロ呼び出し の sequence は translation phase 2 で削除されます から、マクロ展開時には残っていません。 配点 : 2。 [n.24.5] マクロ展開の結果の token separator を残さない 一般にマクロ展開ではその結果が前後の pp-token とくっついてしまわないよ うに何らかの token-separator を前後に挿入することが行われますが([2.4.21] 参照)、しかし、マクロ展開の結果を文字列化する場合は、それらは削除しなけ ればなりません。 規格が token-based な処理を原則としながら character-oriented な部分を 持っているために、こういうややこしい問題が発生します。 配点 : 2。 [e.24.6] # 演算子の operand がパラメータ名でない # 演算子の operand はパラメータ名でなければなりません。 配点 : 2。 [2.4.25] マクロ引数中のマクロの展開 Function-like マクロの呼び出しに際して、引数中にマクロが含まれていた場 合にそれをいつ展開するかについては、K&R 1st. では触れられていませんでし た。Pre-ANSI の処理系での実装もまちまちだったようです。置換リストの再走 査時に展開するものが多かったかもしれません。Standard C ではこれは、引数 を同定したあと、パラメータと置き換える前に展開すると規定されました。関数 呼び出しでの引数の評価と同様の順序で、わかりやすくなりました。ただし、引 数が #, ## 演算子の operand であるパラメータに対応するものの場合は、そこ にマクロ名が含まれていても、それはマクロ呼び出しとはみなされず、展開され ません。* * ANSI C 3.8.3.1 (C90 6.8.3.1) Argument substitution 実引数の置換 C99 6.10.3.1 Argument substitution [n.25.1] 引数中のマクロは先に展開される 引数中のマクロは、引数が同定されたあとで展開され、それから置換リスト中 のパラメータと置き換えられます。したがって、1つの引数として同定されたも のは、たとえ展開すると複数の引数のような形になっても1つの引数です。 配点 : 4。 [n.25.2] 0個のトークンに展開される引数 同様に、展開すると0個のトークンになってしまう引数も、合法的なものです。 配点 : 2。 [n.25.3] ## 演算子を使うマクロを引数中で呼び出す ## 演算子の operand がパラメータの場合、それに対応する引数はマクロ展開 されないので、マクロ展開したい場合はもう1つのマクロをネストさせる必要が あります。 この例では、xglue() は ## 演算子を使っていないので、その引数である glue( a, b) はマクロ展開されて ab となり、xglue() の置換リストは glue ( ab, c) となります。これが再走査されて abc が最終結果となります。 配点 : 2。 [n.25.4] ## 演算子の operand は展開されない こちらは glue() を直接呼び出しているので、その引数中にマクロ名があって も展開されません。 配点 : 6。 [n.25.5] # 演算子の operand は展開されない # 演算子の operand であるパラメータに対応する引数も、マクロ展開されま せん。 配点 : 4。 [e.25.6] 引数中のマクロの展開が引数の範囲で完結しない 引数中のマクロの展開はその引数の中だけで行われます。完結しないマクロは violation of constraint です。Function-like マクロの名前はそれだけではマ クロ呼び出しではありませんが、それに ( が続くと、それがマクロ呼び出し sequence の開始となります。開始されたら最後、この ( に対応する ) がなけ ればなりません。* 配点 : 4。 * ANSI C 3.8.3 (C90 6.8.3) Macro replacement マクロ置換え -- Constraint 制約 C99 6.10.3 Macro replacement -- Constraints [2.4.26] マクロ再走査中の同名マクロ マクロ定義が再帰的になっている場合、そのマクロをそのまま展開してゆくと 無限再帰になってしまいます。このため、K&R 1st. では再帰的なマクロを使う ことができませんでした。Standard C では無限再帰を防ぎながら再帰的マクロ の展開を可能にするため、一度置換したマクロ名は再置換しないという規定が設 けられました。この規定はかなり難解ですが、次のようにパラフレーズできます。 *1 1.マクロ A の置換リストの再走査中にまた A という名前が見つかっても、 それは再置換しない。 2.マクロ A の置換リスト中にマクロ B の呼び出しがあり、マクロ B の置 換の途中に A という名前が出てくるネストされた置換の場合も、その A は再置 換しない。 3.ただし、マクロ B の置換で元のマクロ A の置換リストの後ろのトークン 列が取り込まれた場合は、この取り込まれた部分にある A は置換する。ただし、 置換するのは、この A が(何らかのマクロの置換リスト中ではなく)ソース中 にある場合に限る。 4.マクロ B の置換についても、1から3を再帰的に適用する。すなわち、 マクロ B の置換の途中にまた B が現れ、それが元の B の置換リストの後ろの ソース中のトークン列にあるのでなければ、この B は再置換しない。 5.元のマクロ A の呼び出しの引数中にマクロ C の呼び出しがあり、その置 換中に現れた C が1から3を適用して再置換を禁止された場合、この C は元の マクロ A の置換リストの再走査でも再置換されない。 これだけパラフレーズしてもまだ難解です。ことに3は、後続のトークン列の 取り込みという伝統的なマクロ再走査仕様に由来するもので、これが問題を無用 に煩雑にしています。これについては規格は corrigendum を出したり再訂正し たりしてきていますが、こんがらがる一方です。また、後続するトークン列がソ ース中にある場合とそうでない場合とで展開方法を変えるというのも、一貫性の ない仕様です。*2 後続トークン列を取り込むマクロ展開というものが異常なものである上に、そ の部分にいったん再置換を禁止されたマクロが再度現れるというのは二重に異常 なケースです。この Validation Suite では、この二重に異常なマクロについて のテスト項目は n.27.6 だけしかありません。「後続トークン列の取り込み」と いうマクロ展開の異常な規定が削除されることを、期待したいと思います。*3 *1 ANSI C 3.8.3.4 (C90 6.8.3.4) Rescanning and further replacement 再 走査と再置換 C99 6.10.3.4 Rescanning and further replacement *2 [1.7.6] 参照。 *3 再帰的マクロの展開に関する規格の仕様については、主に2通りの解釈が あり、newsgroup comp.std.c で論争が何回か行われている。recurs.t はこ の論争の素材の1つである。recurs.t 中のコメントを参照されたい。この サンプルは評点の対象とはしない。 V.2.4.1 以降の MCPP の Standard モードでは、再帰的マクロの展開につい ては二つの方法を実装している。デフォルトでは同名マクロ再置換禁止の範 囲を上記 1-5 の説明のとおり広くとり、このサンプルを NIL(42) と展開す る。-c オプションを指定すると再置換禁止の範囲を狭くとり、42 と展開す る。この仕様の違いが出てくるのは、上記の 3 で関数型マクロ A の呼び出 しの前半部分が B の置換リスト中に現れる場合である。デフォルトでは、A の名前が B の置換リスト中に現れただけで A の再置換を禁止するが、-c オプションでは、A の名前だけでなく引数リストとそれを囲む '(' と ')' のすべてが B の置換リスト中に現れた場合だけ、再置換を禁止する。また、 A の名前がソース中にあるかどうかで区別しない。 [n.26.1] 直接再帰の object-like マクロは再展開されない これは object-like マクロの直接再帰の例です。 配点 : 2。 [n.26.2] 間接再帰の object-like マクロも再展開されない Object-like マクロの間接再帰の例です。 配点 : 2。 [n.26.3] 直接再帰の function-like マクロも再展開されない Function-like マクロの直接再帰の例です。 配点 : 2。 [n.26.4] 間接再帰の function-like マクロも再展開されない Function-like マクロの間接再帰の例です。 配点 : 2。 [n.26.5] 引数中の再帰的マクロ Standard C には「一度再置換を禁止されたマクロは、別の文脈で再走査され ても置換されない」という意味の難解な規定がありますが、具体的にこれに該当 するのは引数中のマクロの親マクロ再走査時の扱いです。引数中に再帰的マクロ があった場合は、1度しか置換されませんが、それは親マクロの再走査でも置換 されません。 配点 : 2。 [2.4.27] マクロの再走査 マクロの置換リストが再走査されるのは K&R 1st. 以来の仕様です。再走査時 に発見されたマクロは再帰的マクロでない限り、置換されます。これによって、 ネストされたマクロ定義が処理されます。Standard C では特に変更があったわ けではありませんが、K&R 1st. では明確でなかったところがいくらか明確にな っています。* * ANSI C 3.8.3.4 (C90 6.8.3.4) Rescanning and further replacement 再 走査と再置換え C99 6.10.3.4 Rescanning and further replacement [n.27.1] Object-like マクロのネスト これは K&R 1st. でも同じです。 配点 : 6。 [n.27.2] Function-like マクロのネスト これも K&R 1st. でも同じです。 配点 : 4。 [n.27.3] ## 演算子で生成された名前も展開の対象となる ## 演算子の operand に対応する引数はマクロ展開されませんが、pp-token の連結によって生成された新たな pp-token は、再走査時にマクロ展開の対象と なります。 配点 : 2。 [n.27.4] 置換リスト中に形成された function-like マクロ Function-like マクロの名前が出てきても、それに ( が続いていない場合は、 それはマクロ呼び出しとはみなされません。引数から function-like マクロの 名前が取得され、置換リスト中にその名前を使って function-like マクロの呼 び出しが形成されると、それは展開されます。 配点 : 4。 [n.27.5] Function-like マクロの前半を形成する置換リスト 置換リストが function-like マクロの呼び出しの前半を形成するという異常 なマクロは、pre-Standard では暗黙の仕様でしたが、Standard C では公認され てしまいました。置換リストの後ろの pp-token 列が取り込まれて、マクロ呼び 出しが完結します。 配点 : 2。 [n.27.6] 再置換される同名マクロ 再走査では一般に同名のマクロは再置換されませんが、再置換される場合もあ ります。ネストされたマクロ呼び出しで再走査が置換リストの後ろのソース中の pp-token 列を取り込み、その中に同名マクロが現れるという異常な場合です ([2.4.26] を参照)。 配点 : 2。 [e.27.7] 再走査時に引数の数が合わない Function-like マクロの呼び出しでは引数の数がパラメータの数と一致してい なければなりませんが、置換リストの再走査時に発見された function-like マ クロでも、もちろん同様です。しかし、引数は , で区切られるものなので、ト リッキーなマクロでは引数の数が直観的にわかりにくいことがあります。 配点 : 2。 [2.4.28] 事前定義マクロ C90 では5つの特殊なマクロを処理系が事前定義することになりました。*1 さらに C90 / Amendment 1 では、__STDC_VERSION__ というマクロも追加され ました。 __FILE__, __LINE__ は動的に定義が変化してゆく、きわめて特殊なマクロで す。assert() マクロやデバッグ用ツール等での使い道があります。その他の標 準事前定義マクロは1つの translation unit の処理中は変化しません。 *1 ANSI C 3.8.8 (C90 6.8.8) Predefined macro names あらかじめ定義され たマクロ名 *2 C99 6.10.8 Predefined macro names [n.28.1] __FILE__ プリプロセス中のソースファイル名を表す文字列リテラルに定義されます。# include によって取り込まれたソースファイルではそのファイル名になるので、 同一の translation unit 中でも変化します。 配点 : 4。"n_28.t" という文字列リテラルでなく n_28.t といった単なる名 前になるものは 0 点。 [n.28.2] __LINE__ プリプロセス中のソースファイルの行番号を表す10進定数に定義されます。 行番号は1から始まります。行番号は物理行の番号です。 配点 : 4。行番号が 0 から始まるものは 2 点。 [n.28.3] __DATE__ プリプロセスの行われている日付を表す文字列リテラルに定義されます。 asctime() 関数の生成するものとほぼ同じ "Mmm dd yyyy" の形ですが、日が 10 日未満の場合は dd の1ケタ目は 0 ではなく space になるところが違っていま す。 配点 : 4。10 日未満の場合の dd の1ケタ目が 0 となるものは 2 点。 [n.28.4] __TIME__ プリプロセスの行われている時刻を表す文字列リテラルに定義されます。 asctime() 関数の生成するものと同じ "hh:mm:ss" の形です。 配点 : 4。 [n.28.5] __STDC__ C90, C99 準拠の処理系では定数 1 に定義されます。 配点 : 4。 [n.28.6] __STDC_VERSION__ C90 / Amendment 1:1995 に対応した処理系では、これが 199409L に定義され ます。*1 配点 : 4。 *1 Amendment 1 / 3.3 Version macro(ISO 9899:1990 / 6.8.8 への追加) [n.stdmac] C99 の事前定義マクロ C99 では __STDC_VERSION__ の値は 199901L です。 また、__STDC_HOSTED__ という事前定義マクロが追加されました。これは処理 系が hosted implementation であれば 1 に、そうでなければ 0 に定義されま す。 配点 : 4。各 2 点。 [n.28.7] インクルードされたファイル中の __LINE__ 等 __FILE__, __LINE__ は translation unit ではなくソースファイルを対象と するので、include されたソース中ではその include ファイルの名前と行番号 になります。 配点 : 4。行番号が 0 から始まるものは 2 点。 [2.4.29] #undef #undef は K&R 1st. あるもので、大きな変化はありません。指定されたマク ロの定義が取り消されます。マクロは同一の translation unit 中で、#define で定義されてから #undef で取り消されるまでの間が有効範囲です。* * ANSI C 3.8.3.5 (C90 6.8.3.5) Scope of macro definitions マクロ定義 の有効範囲 C99 6.10.3.5 Scope of macro definitions [n.29.1] #undef によるマクロの取り消し #undef された後ではそのマクロ名はもはやマクロではありません。 配点 : 10。 [n.29.2] 定義されていないマクロの #undef マクロとして定義されていない名前を #undef することは許されています。処 理系はこれをエラーにしてはいけません。 配点 : 4。 [e.29.3] 名前がない #undef 行には identifier が必要です。 配点 : 2。 [e.29.4] 余計なトークンがある #undef 行には1つの identifier 以外のものがあってはいけません。 配点 : 2。 [e.29.5] 引数がない #undef 行に引数がないのも violation of syntax rule です。 配点 : 2。 [2.4.30] マクロ呼び出し マクロ呼び出しに際しては、改行は単なる white spaces の1つとして扱われ ます。したがって、マクロ呼び出しは複数の行にまたがることができます。これ は K&R 1st. では明確ではありませんでした。* 関数様マクロ呼び出しの引数というのは , で区切られたものです。しかし、 ( と ) のペアの中に入っている , は引数を区切るものとはみなされません。こ のことはここでは直接はテストしませんが、n.25 を初め、あちこちで暗黙のう ちにテストされています。また、*.c のサンプルの多くは assert() マクロを使 っているので、この点に関してはかなり複雑なテストがされることになります。 * ANSI C 3.8.3 (C90 6.8.3) Macro replacement マクロ置換え -- Semantics 意味規則 C99 6.10.3 Macro replacement -- Semantics [n.30.1] マクロ呼び出しは複数行にまたがることができる 配点 : 6。 [n.nularg] マクロ呼び出しのカラ引数 C99 ではマクロ呼び出しでカラ引数が認められました。これは引数の過少とは 異なります。引数を分離する ',' は省略できません(パラメータが一つの場合 はこの両者を区別できないが)。 配点 : 6。 [2.4.31] マクロ呼び出しのエラー 次はマクロ呼び出しのいくつかのエラーです。 [e.31.1] 引数が多すぎる 引数の数がパラメータの数と違っているのは violation of constraint です。 Undefined ではありません。* 配点 : 2。 * ANSI C 3.8.3 (C90 6.8.3) Macro replacement マクロ置換え -- Constraint 制約 C99 6.10.3 Macro replacement -- Constraints [e.31.2] 引数が足りない 引数の数がパラメータの数より少ないのも violation of constraint です。 C99 ではカラ引数が認められましたが、これは引数の過小とは異なります。引 数を分離する ',' が欠如していてはいけません。 配点 : 2。 [e.31.3] コントロール行で完結しないマクロ呼び出し 一般にはマクロ呼び出しは複数の行にまたがることができますが、# で始まる preprocessing directive line はその行(コメントを a space に変換した後の 行)で完結するので、その中にあるマクロ呼び出しもその行の中で完結していな ければなりません。 配点 : 2。 [e.vargs2] 可変引数マクロの引数がない C99 の可変引数マクロでは __VA_ARGS__ に対応する実引数は、少なくとも1 つは必要です。 配点 : 2。 [2.4.32] #if 式の文字定数 #if 式では文字定数を使うことができます。しかし、その評価はほとんど implementation-defined で、ポータビリティはあまりありません。32.? では最 も単純な1バイトの文字定数(single-byte character constant)を取り上げま す。*1 この Validation Suite の i_* のサンプルはすべて、#if 式での基本文字セ ットが ASCII であることを前提としています。 *1 ANSI C 3.1.3.4 (C90 6.1.3.4) Character constants 文字定数 ANSI C 3.8.1 (C90 6.8.1) Conditional inclusion 条件付き取り込み -- Semantics 意味規則 C99 6.4.4.4 Character constants C99 6.10.1 Conditional inclusion -- Semantics 以下、33, 34, 35 のテストの典拠も同じである。 [n.32.1] Character octal escape sequence 文字定数では8進 escape sequence を使うことができます。これに関しては どの処理系でも同じで、implementation-defined な部分はありません。 配点 : 2。 [n.32.2] Character hexadecimal escape sequence 文字定数では16進 escape sequence も使うことができます。これも implementation-defined な部分はありません。16進 escape sequence は K&R 1st. にはなかったものです。 配点 : 2。 [i.32.3] Single-byte character constant Escape sequence ではない1バイトの文字定数は単純なものですが、基本文字 セットによって値が異なります。コンパイル時と実行時とで基本文字セットが違 うクロスコンパイラでは、#if 式の評価ではそのどちらを使ってもかまわないこ とになっています。 また、同じ基本文字セットであっても、符号の扱いは implementation- defined で、しかもコンパイラ本体(translation phase 7)とプリプロセス (phase 4)とで扱いが異なっていてもかまわないことになっています。 したがって、#if 式で文字定数の値を見て基本文字セットを判定することは、 保証されている方法ではありません。 配点 : 2。 [i.32.4] '\a', '\v' Standard C では '\a', '\v' という escape sequence が追加されました。 配点 : 2。 [e.32.5] Escape sequence の値が unsigned char の範囲外 文字定数中の1つの escape sequence は1つの single-byte character の値 を表すものなので、unsigned char の範囲になければなりません。 配点 : 2。 [2.4.33] #if 式のワイド文字定数 ワイド文字定数は Standard C で新設されたものです。その値の評価は single-byte character constant にも増して implementation-defined で、バ イトの評価オーダーさえも決められていません。 ワイド文字には各種の encoding がありますが、それは [3.1] でとりあげま す。ここでは ASCII 文字に対応するワイド文字定数だけとりあげます。 [e.33.2] ワイド文字定数の値が範囲外 ワイド文字定数でも16進または8進の escape sequence が使えますが、そ の値はワイド文字1字の値を符号なしで表現した範囲になければなりません。 配点 : 2。 [2.4.35] #if 式の multi-character 文字定数 文字定数には multi-character character constant というものもあります。 Multi-byte character constant と紛らわしい用語ですが別の概念で、複数の character からなる文字定数を意味します。この character には、single-byte character, multi-byte character, wide character があり、それぞれに対応す る multi-character character constant があるのです(規格書では character という用語は single-byte character の意味で使われているが、ここでは3種 の文字を指す言葉として使う)。 Multi-character character constant の使い道は特に何もなさそうです。こ れが K&R 1st. 以来認められているのは単に、character constant の型は int だから int の範囲に入るものであれば何でもかまわない、ということにすぎな いと思われます。 [i.35.1] multi-character 文字定数の評価 Single-byte character の multi-character character constant は、K&R 1st. からあったものです(A.16)。しかし、評価のバイトオーダーは K&R 1st. でも Standard C でも規定されていません。 配点 : 2。 [e.35.2] multi-character 文字定数の値が範囲外 16進または8進 escape sequence による multi-character character constant の値は通常は int または unsigned int の範囲になければならないと 考えられます。しかし、C90 では #if 式では int / unsigned int は long / unsigned long と同じ内部表現を持つかのように扱われるので、long または unsigned の範囲にあるかどうかをチェックすればすむと考えられます。しかし、 この点は規格は明確ではありません。値の評価の仕方そのものが implementa- tion-defined であるので、範囲チェックも implementation-defined であると も解釈できます。 いずれにしても、このサンプルは long が64ビット以下の処理系ではどう評 価しても unsigned long の範囲を超えるので、診断メッセージを出すのが適当 でしょう。 C99 では #if の型はその処理系の最大の整数型となりました。 配点 : 2。 [i.35.3] Multi-character ワイド文字定数の評価 ワイド文字の multi-character constant というものも存在します。評価の仕 方はやはり全面的に implementation-defined ですが、対応する multi-byte character の multi-character constant と合っている必要があります。 配点 : 2。 [2.4.37] Translation limits Standard C では、処理系が扱うことのできる各種 translation limits の最 低限が規定されました。しかし、この規定はかなり緩やかなものです。すなわち、 22種の限界値についてそれぞれ、それを満たす例を1つ以上含むプログラムを 処理でき、実行できなければならない、というものです。この Validation Suite のサンプルを見ればわかるように、このプログラムは処理系にとって最も 負担が少なくなるように、きわめて単純に非実際的に書くことができます。これ らの translation limits が常に保証されるわけではないことに、注意してくだ さい。Translation limits の規定は目安にすぎないと言えます。このサンプル はプリプロセスの関係する8種の translation limits のテストだけをするもの です。*1, *2, *3 なお、これらのテストサンプルの一部は画面内におさまるように行を折り返し ています。処理系によっては、行接続等の処理が正しく行えないためにこれらの テストに失敗することがあります(Borland C 等)。行接続等のテストがここで の目的ではないので、失敗した場合はエディタで行をつないで再テストしてくだ さい。 *1 ANSI C 2.2.4.1 (C90 5.2.4.1) Translation limits 翻訳限界 *2 C99 5.2.4.1 Translation limits C99 では、translation limits は大幅に拡大されている。C++ Standard ではさらに大きい([3.6] 参照)。 [n.37.1] マクロ定義中の31個のパラメータ C90 では、マクロ定義中のパラメータは31個までが一応保証されています。 配点 : 2。 [n.37.2] マクロ呼び出し中の31個の引数 同様に C90 では、マクロ呼び出し中の引数は31個までが一応保証されてい ます。 配点 : 2。 [n.37.3] 31文字の識別子名 C90 では、Translation unit の内部的な identifier(マクロ名もこれに含ま れる)は先頭の 31 characters が有意であることが保証されています。プリプ ロセスはもちろん、31 バイトの名前を通す必要があります。* 配点 : 4。 * ANSI C 3.1.2 (C90 6.1.2) Identifiers 識別子 -- Implementation limits 処理系限界 [n.37.4] 8重の条件取り込み C90 では、#if (#ifdef, #ifndef) section のネストは8重までが一応保証さ れています。 配点 : 2。 [n.37.5] 8重の #include C90 では、#include のネストは8重までが一応保証されています。 配点 : 4。 [n.37.6] 32重のカッコ付き #if 式 C90 では、式中の (, ) のネストは32重までが一応保証されています。これ は #if 式にも適用されると考えられます(#if 式では一般の式と違ってそこま で保証する必要はないとも思われる。しかし、#if 式は整数定数式で long / unsigned long だけで評価されることと、実行時環境への問い合わせを必要とせ ず、実行時や phase 7 と同じ評価の仕方でなくてもよいということだけが例外 として規定されていて、他の側面では特別扱いされていないので、やや過剰な規 定になっているところもある)。 配点 : 2。 [n.37.7] 509 バイトの文字列リテラル C90 では、文字列リテラルの長さは 509 バイトまでが一応保証されています。 この長さはトークンの長さであり、char 配列の要素数ではありません。両端の " を含み、\n 等は2バイトと数えます。ワイド文字列リテラルでは L という prefix も含みます。 配点 : 2。 [n.37.8] 509 バイトの論理行 C90 では、論理行の長さは 509 バイトまでが一応保証されています。 配点 : 2。 [n.37.9] 1024 個のマクロ定義 C90 では、マクロ定義は 1024 個までが一応保証されています。しかし、 translation limits の規定でもこれが最もあいまいなものです。このサンプル のように最も簡単なマクロばかりの場合と、長大なマクロを多く含む場合とで、 処理系の必要とするメモリの量はまったく異なります。また、1024 個に事前定 義マクロを含むかどうかでも、テストプログラムが違ってきます。実際のプログ ラムでは、ユーザプログラムでマクロを定義する前に、標準ヘッダで多くのマク ロが定義されます。この規定はまさに大ざっぱな目安にすぎません。実際の限界 はシステムの提供できるメモリ量によって決まるでしょう。 配点 : 4。 [n.tlimit] C99 の translation limits C99 では translation limits が大幅に拡大されました。 配点 : 以下のそれぞれについて 2。 [n.37.1L] マクロ定義中の 127 個のパラメータ [n.37.2L] マクロ呼び出し中の 127 個の引数 [n.37.3L] 63 文字の識別子名 [n.37.4L] 63 重の条件取り込み [n.37.5L] 15 重の #include [n.37.6L] 63 重のカッコ付き #if 式 [n.37.7L] 4095 バイトの文字列リテラル [n.37.8L] 4095 バイトの論理行 [n.37.9L] 4095 個のマクロ定義 [2.5] 処理系定義部分のドキュメント Standard C には implementation-defined(処理系定義)と呼ばれる部分があ ります。この部分の仕様は処理系によって異なります。しかし、処理系はその仕 様をドキュメントに記載しなければならない、とされています。* Implementation-defined とされるものの中には、言語処理系そのものが決め るもののほかに、CPU やOSによって決まる部分も含まれています。クロスコン パイラの場合、CPU やOSは翻訳時と実行時とで異なる場合もあります。 以下の項目はプリプロセスに関する implementation-defined な部分について、 処理系のドキュメントに記載があるかどうかをチェックするものです。プリプロ セスですから、CPU やOSはもちろん翻訳時のものです。d.1.* はプリプロセス 固有の仕様であり、d.2.* はコンパイラ本体の仕様とも関係のあるものです。し かし、#if での式の評価は、コンパイラ本体とは仕様が異なってもかまわないこ とになっています。 以下の項目のほかにも、#if 式の評価には implementation-defined な側面が いくつかあります。まず、文字セットです(基本文字セットが ASCII か EBCDIC か等)。また、整数の encoding(2の補数、1の補数、符号+絶対値)もそう です。さらに、通常の算術変換によって符号つき整数が符号なしに変換された結 果もそうです。しかし、これらはいずれもマシンとOSで決まるものであるので そちらのドキュメントがあれば足り、言語処理系のドキュメントには記載はなく てもかまわないと考えられます。したがって、ここではこれらは評点の対象とし ません。 * ANSI C 1.6 (C90 3) Definitions of Terms 用語の定義及び規約 C99 3 Terms, definitions, and symbols [d.1.1] Header-name を構築する方法 Header-name というのは特殊な pp-token です。<, > または ", " で囲まれ た sequence をどうやって header-name という1つの pp-token に結合するの かは implementation-defined となっています。実装上は ", " で囲まれたもの は文字列リテラルという pp-token として扱えばすむので簡単なのですが、<, > で囲まれたものはきわめて特殊な問題を持っています。 は trans- lation phase 3 で <, stdio, ., h, > という5つの pp-token にいったん分解 された上で、phase 4 で1つの pp-token に合成されることになるからです。こ の部分がマクロで書かれている場合は、さらに微妙な問題が生じます。* 配点 : 2。すなわち、この仕様が処理系のドキュメントに記載されていれば 2 点、記載されていなければ 0 点。 なお、header-name 中の大文字・小文字の区別の有無やファイル名の規則も implementation-defined であるが、これはOSで決まることなので、言語処理 系のドキュメントには必ずしも記載する必要はないと考えられる。 * ANSI C 3.8.2 (C90 6.8.2) Source file inclusion ソースファイル取り込 み -- Semantics 意味規則 C99 6.10.2 Source file inclusion [d.1.2] #include でヘッダをさがす方法 #include 行から header-name が取り出された後、その header file をどう やってさがすのかも implementation-defined です。", " で囲まれた header- name の場合は、まず implementation-defined な方法でファイルがサーチされ、 見つからなかった時は <, > で囲まれた header-name と同様にサーチされるこ とになっています。ところがこの後者もまた implementation-defined なのです。 一向に要領を得ない規定ですが、これは Standard C がOSについて前提を置く ことができないため、こうした表現にならざるを得ないのです。 ディレクトリ構造を持ったOSでは、前者はカレントディレクトリからの相対 パスを、後者は処理系の既定のディレクトリをサーチすると解されます。ただし、 前者では include 元のファイルからの相対パスをサーチする処理系もあります。 Implementation-defined であるからには、これも間違いとは言えません。これ について Rationale は、カレントディレクトリからの相対パスでサーチする仕 様が委員会の意図ではあるが、OSについて前提を置くことができないので明文 化できなかった、と説明しています。*1 また、前者のサーチはサポートされなくても良い(", " で囲まれた header- name も <, > とまったく同じに扱っても良い)とされています。後者は必ずし もファイルでなくてもよい、とされています。処理系に組み込みの header もあ りうるということです。*2 配点 : 4。すなわち、これらの header の捜し方がドキュメントに十分に記 載されていれば 4 点、不十分な記載しかなければ 2 点、ほとんど記載されてい なければ 0 点。 *1 ANSI C Rationale 3.8.2 Source file inclusion *2 ANSI C 3.8.2 (C90 6.8.2) Source file inclusion ソースファイル取り 込み -- Semantics 意味規則 C99 6.10.2 Source file inclusion [d.1.3] #include のネストの限度 #include のネストがどれだけできるかは implementation-defined です。た だし、少なくとも C90 では8レベル、C99 では 15 レベルは保証しなければな りません。*1, *2 配点 : 2。 *1 ANSI C 2.2.4.1 (C90 5.2.4.1) Translation limits 翻訳限界 C99 5.2.4.1 Translation limits *2 ANSI C 3.8.2 (C90 6.8.2) Source file inclusion ソースファイル取り 込み -- Semantics 意味規則 C99 6.10.2 Source file inclusion [d.1.4] 実装されている #pragma sub-directive #pragma という preprocessing directive は処理系固有の拡張機能を指定す るために用意されている directive です。プリプロセスにおいても、拡張機能 はすべて #pragma sub-directive として実装しなければなりません。* 配点 : 4。その処理系で有効な #pragma sub-directive について(プリプロ セスのドキュメントでは少なくともプリプロセスのためのすべての #pragma sub- directive について)ドキュメントに十分な記載があれば 4 点、不十分な記載 しかなければ 2 点、ほとんど記載がなければ 0 点。また、#pragma sub- directive 以外の処理系固有の directive がある場合は 2 点を減ずる(ただし、 0 点が下限。Standard C に最も近い仕様を指定するオプションによって禁止さ れる directive は含まない)。 * ANSI C 3.8.6 (C90 6.8.6) Pragma directive プラグマ指令 C99 6.10.6 Pragma directive [d.pragma] #pragma でのマクロ展開 C90 では、 #pragma 行の pp-token はマクロ展開の対象となりませんでした。 しかし、C99 では #pragma に STDC という token が続くものはマクロ展開の対 象となりませんが、それ以外の #pragma sub-directive をマクロ展開するかど うかは implementation-defined となりました。 配点 : 2。 [d.1.5] 事前定義マクロ __FILE__, __LINE__, __DATE__, __TIME__, __STDC__, __STDC_VERSION__ (C99 では __STDC_HOSTED__ も)以外の事前定義マクロは implementation- defined です。それらは1つの _ に大文字が続く名前、または2つの _ で始ま る名前でなければなりません。* 配点 : 4。不十分な記載しかなければ 2 点、規定の制限に反する名前の事前 定義マクロがある場合は 2 点を減ずる(ただし、0 点が下限。Standard C に最 も近い仕様を指定するオプションによって禁止されるマクロは評価の対象としな い)。 * ANSI C 3.8.8 (C90 6.8.8) Predefined macro names あらかじめ定義され たマクロ名 C99 6.10.8 Predefined macro names [d.predef] C99 の事前定義マクロ C99 では、__STDC_IEC_559__, __STDC_IEC_559_COMPLEX__, __STDC_ISO_10646__ というマクロが条件によって事前定義されるものとして追 加されています。 __STDC_IEC_559__, __STDC_IEC_559_COMPLEX__ は IEC 60559 浮動少数点規格 に合致する実装ではそれぞれ 1 に定義するとされています。この2つは浮動小 数点演算のライブラリによって決まるもので、実際には 等で定義する のが適当かもしれません。必ずしもプリプロセッサで事前定義する必要はないと 考えられます。 __STDC_ISO_10646__ は wchar_t 型の文字の値がすべて ISO/IEC 10646 (Unicode 系の Universal Character Set)の何らかのコード化された実装であ る場合は、準拠する ISO/IEC 10646 の amendment や corrigendum を含めた規 格の年月を表す 199712L といった形の定数に定義するとされています。これも 等で定義することが考えられ、プリプロセッサで事前定義する必要は なさそうです。 しかし、どちらにしてもドキュメントで説明されていることは必要です。 配点 : 6。3つのそれぞれについて各 2 点。 [d.1.6] Phase 3 で white-spaces を圧縮するか Standard C では translation phase 3 で tokenization を行いますが、その 時に 以外の white-space sequence を one space に圧縮するかしな いかは implementation-defined とされています。* しかし、これは通常はコンパイル結果には影響を与えない処理系の内部仕様な ので、ユーザが関知する必要はないものです。行頭と行末の white-spaces にい たっては、削除してもかまいません。 ではこの規定は無用かと言うと、いつもそういうわけではなく、必要な場合が 1つだけあります。それは preprocessing directive 行に [VT], [FF] がある 場合です。これについては Standard C はわかりにくい規定の仕方をしています。 一方でこれを violation of constraint とし、他方では上記の規定を設けてい るのです。すなわち、phase 3 で [VT], [FF] をその前後の space, tab と合わ せて one space に圧縮することができ、その場合は phase 4 には [VT], [FF] は残らないが、圧縮しなかった場合はこれが残って violation of constraint となるのです。 ドキュメントには、この [VT], [FF] の扱いが書かれていれば十分だと考えら れれます。 配点 : 2。 * ANSI C 2.1.1.2 (C90 5.1.1.2) Translation phases 翻訳フェーズ 3 ANSI C 3.8 (C90 6.8) Preprocessing directive 前処理指令 -- Constraints 制約 C99 6.10 Preprocessing directive [d.ucn] 文字列化で UCN の \ を重ねるか UCN を含む pp-token を # 演算子によって文字列化する場合、UCN の \ を重 ねるかどうかは implementation-defined です。 配点 : 2。 * C99 6.10.3.2 The # operator [d.2.1] #if: Single-character character constant の評価 一般に文字定数の値の評価は implementation-defined です。これにはいくつ かの次元があります。 1.基本文字セットは何か 2.多バイト文字・ワイド文字の encoding は何か 3.符号の扱いはどうか 4.複数バイトの文字定数の評価のバイトオーダーはどうか このうち1はハードウェアとOSで決まるものなので、ここでは評点の対象と しません。問題は2,3,4です。 1バイトの single-character の文字定数でも、符号の扱いは implemen- tation-defined です。また、プリプロセスとコンパイルとで評価が違っていて も良いことになっています。* 配点 : 2。ドキュメントに記載がある場合、またはコンパイルフェーズでの 評価についての記載があり #if 式でも同じ評価がされる場合は 2 点、正しい記 載がない場合は 0 点。 * ANSI C 3.1.3.4 (C90 6.1.3.4) Character constants 文字定数 ANSI C 3.8.1 (C90 6.8.1) Conditional inclusion 条件付き取り込み -- Semantics 意味規則 C99 6.4.4.4 Character constants C99 6.10.1 Conditional inclusion -- Semantics [d.2.2] #if: Multi-character character constant の評価 'ab' といった multi-character 文字定数の評価には、バイトオーダーの問題 があります。これも implementation-defined です。 配点 : 2。評点の仕方は d.2.1 と同様。 [d.2.3] #if: Multi-byte character constant の評価 多バイト文字定数の評価は encoding の違いのほかに、符号の扱い、バイトオ ーダーの違いがあり、これらはいずれも implementation-defined です。 配点 : 2。評点の仕方は d.2.1 と同様。 [d.2.4] #if: ワイド文字 character constant の評価 ワイド文字定数の評価にも encoding の違いのほかに、符号の扱い、バイトオ ーダーの違いがあり、これらはいずれも implementation-defined です。 配点 : 2。評点の仕方は d.2.1 と同様。 [d.2.5] #if: 負数の右シフト 一般に負の整数の右ビットシフトで符号ビットがどう扱われるかは implemen- tation-defined です。これは CPU の仕様にもよりますが、言語処理系の実装方 法にもよると思われます。* 配点 : 2。評点の仕方は d.2.1 と同様。 * ANSI C 3.3.7 (C90 6.3.7) Bitwise shift operators ビット単位のシフト 演算子 -- Semantics 意味規則 C99 6.5.7 Bitwise shift operators -- Semantics [d.2.6] #if: 負数の除算・剰余算 一般に右辺または左辺の片方または双方が負の整数である場合の、除算と剰余 算の結果は implementation-defined です。これは CPU の仕様にもよりますが、 言語処理系の実装方法にもよると思われます。*1, *2 配点 : 2。評点の仕方は d.2.1 と同様。 *1 ANSI C 3.3.5 (C90 6.3.5) Multiplicative operators 乗除演算子 -- Semantics 意味規則 C99 6.5.5 Multiplicative operators -- Semantics *2 C99 では、div(), ldiv() と同様に商は 0 の方向に切り捨てられること になった。 [d.2.7] 識別子の有効長 マクロ名を含む identifier の先頭から何バイトまでが有意であるかは implementation-defined です。ただし、マクロ名と内部的識別子については、 C90 では 31 バイト、C99 では 63 バイトは保証しなければなりません。* 配点 : 2。 * ANSI C 3.1.2 (C90 6.1.2) Identifiers 識別子 -- Implementation limits 処理系限界 C99 6.1.2 Identifiers -- General -- Implementation limits [d.mbident] 識別子中の multi-byte character C99 では、identifier 中に multi-byte character を使える実装も許される ことになりました。これは implementation-defined です。* 配点 : 2。 * C99 6.1.2 Identifiers -- General ☆ 3.規格で要求されていない諸側面の評価 ☆ 規格が処理系に対して要求していないことであっても、処理系の「品質」を評 価するために重要なことがいろいろあります。この章では、これらの品質評価事 項のテストを解説します。 [3.1] Multi-byte character encoding Multi-byte character には各種の encoding があります。その仕様は implementation-defined ですが、ここでは処理系がどれだけ多様な encoding にどこまで対応しているかのテストを「品質」の問題としてとりあげます。 この Validation Suite では、m_33, m_34, m_36 については数種の encoding に対応したサンプルを用意しています。処理系はそのシステムでの標準の encoding に対応していなければならないのはもちろんですが、多言語に対応し たソースを処理するためには、多くの encoding に対応している必要があります。 * しかし、それをテストする方法はシステムや処理系によって異なり、簡単では ありません。 GNU C は環境変数 LC_ALL, LC_CTYPE, LANG に対応して動作を変えることにな っていますが、実装が中途半端であてにできません。また、GNU C でこの機能が 使えるかどうかは GNU C 自身をコンパイルした時の configuration によって異 なります。 さらに GNU C V.3.4 では multi-byte character の処理がガラリと変わりま した。プリプロセスの最初に(translation phase 1 に相当する時期に)ソース ファイルの encoding を UTF-8 に変換してしまいます。こうなると、#if 式中 の文字定数は UTF-8 でないと評価できず、元の encoding とは関係なくなって しまいます。 C++98 の規格にも同様の問題があり、translation phase 1 で multi-byte character を UCN に変換してしまうので、#if 式中の文字定数は UCN としてで ないと評価できないことになります。 規格と実装のいささか混乱した状況と、一般に #if 式中の文字定数というも のの portability の無さと意味の無さを踏まえて、この Validation Suite で は V.1.5 からは #if 式中の multi-byte/wide character の文字定数のテスト は評点の対象からはずしました(m.33.1, m.34, m.36.1)。 Visual C++ には #pragma setlocale という便利なディレクティブがあるので、 これが使えます。Windows では「地域と言語のオプション」によって使用言語を 変更できることになっていますが、中途半端でありやっかいです。#pragma setlocale は Windows をいじらずに使えるので、プログラマにとっては便利な ものです(Visual C++ 自身がそれをどこまで正しく実装しているかは別である が)。 私のテストした他の処理系では今のところ、その処理系でのデフォルトの encoding にしか対応していないようです。ライブラリに setlocale() 等の関数 を持つ処理系は多くありますが、それはソースのプリプロセスやコンパイルには 関係ありません。ここで必要なのは、処理系自身がソースの encoding を認識し 処理する能力なのです。 * C99 では、\u, \U で始まる Unicode の sequence が導入され、multi- byte / wide character との関係がきわめてわかりにくくなってしまってい る。C++ Standard ではさらに複雑である。 [m.33.1] ワイド文字定数の評価 ワイド文字定数については、[2.4.33] を参照してください。 配点 : なし。 [m.34] #if 式の multi-byte 文字定数の評価 #if 式では複数バイトの multi-byte character constant(多バイト文字定数) も使えます(規格書では multi-byte character という用語は single-byte character も含むものとして使われているが、ここでは混乱しないように、 single-byte ではない multi-byte character を指すことにする)。しかし、こ の評価も single-byte character constant 以上に implementation-defined で す。 配点 : なし。このテストは後述する u.1.7 のテストと合わせて判断する必 要がある。単に文字の値を評価できただけでは、その encoding を認識している ことにはならない。u.1.7 は multi-byte character がその encoding で認めら れる範囲に収まっているかどうかのテストである。m.34 で文字の値を正しく評 価した上で、さらに u.1.7 で適切な診断メッセージを出して初めてその encoding を認識していると言える。 [m.36.1] Multi-byte character 中の 0x5c は escape 文字ではない (Multi-byte | wide) character の encoding が shift-JIS, Big-5, ISO- 2022-* 等の場合は、文字の中に '\\' と同じ 0x5c の値を持つバイトが出てく ることがあるため、わずらわしい問題が生じます。処理系はこれを escape 文字 としての \ (backslash) と解釈してはいけません。1つの (multi-byte / wide) character は1つの文字であり、2つの single-byte character ではないから です。 Multi-byte character に値が 0x5c のバイトがあっても、これを escape sequence の始まりと解釈してはいけません。 配点 : なし。 [m.36.2] # 演算子は multi-byte character に \ を挿入しない # 演算子の operand に対応する引数中に 0x5c の値のバイトを持つ multi- byte character が含まれていても、そこに \ を挿入してはいけません。もっと も、コンパイラ本体が multi-byte character に対応していない場合にプリプロ セッサで \ を挿入することで対応させるという方法がありますが、それはまた 別の次元のことです。 また、そもそもこの種の multi-byte character を含む文字定数や文字列リテ ラルの tokenization にも、その他のリテラルとは違った面倒な問題があります。 ISO-2022-* で encode された multi-byte character には '\\' ばかりでな く '\'', '"' と一致する値のバイトも含まれるので、いい加減な処理をすると tokenization に失敗します。 配点 : 7。Shift-JIS, Big-5 の encoding について各 2 点。ISO-2022-JP については3つのサンプルについて各 1 点。 なお、この項目は m_36_*.t だけでなく m_36_*.c もテストする必要がある。 文字列化だけであれば正しく処理するプリプロセッサが、assert() マクロで 0x5c のバイトを持つ漢字を含む複雑な文字列リテラルを引数として同定するこ とに失敗することがあるからである。m_36_*.c は文字列リテラルの tokenization のテストにもなっている。 [3.2] Undefined behavior Standard C には undefined behavior という規定がたくさんあります。 Undefined behavior を引き起こすのは、間違ったプログラムまたはデータ、あ るいは少なくとも移植性のないプログラムですが、violation of syntax rule or constraint とは違って、処理系はこれに対して診断メッセージを出すことを 義務付けられていません。これを処理する時は、処理系はどう処理してもかまわ ないのです。正しいプログラムとして何らかの reasonable な処理をしても良く、 診断メッセージを出してエラーにしても良く、診断メッセージを出さずに処理を 中止したり暴走したりしても規格違反ではありません。 しかし、処理系の「品質」を評価するには、undefined behavior が具体的に どういうものであるかが問題になります。処理系は何らかの診断メッセージを出 すのが適当でしょう。エラーにするのであればもちろんのこと、正しいプログラ ムとして扱う場合でもウォーニングを出すことが、プログラムに移植性がないこ とを知らせるために有用です。暴走するのは論外です。 以下のテストでは、undefined behavior を引き起こすソースに対して処理系 が適切な診断メッセージを出すかどうかをチェックします。診断メッセージはエ ラーでもウォーニングでもかまいません。もちろん、ウォーニングの場合は何ら かの reasonable な処理をする必要があります。 u.1.* はプリプロセス固有の問題で、u.2.* は定数式一般に共通する問題です。 配点 : 適切な診断メッセージが出されれば、以下の各項目のうち、特に断 りのない項目については 1 点。見当外れな診断メッセージである場合や診断メ ッセージが出されない場合は 0 点。 [u.1.1] ソースファイルが で終わっていない 空でないソースファイルの最後が でないのは、undefined behavior を引き起こします(もっとも、OSによってはファイル中に改行文字 というデータは存在せず、ファイルを読み込む時に処理系によって自動的に付加 されるのだそうである)。* u.1.1, u.1.2, u.1.3, u.1.4 はいずれもソースファイルが完結していないも のですが、translation unit がそのファイルで終わっている場合は、診断メッ セージを出す処理系が多いと思われます。しかし、そうした処理系でも、このフ ァイルが include されたものである場合、include 元のファイルと連続して処 理されることで、正常なソースとして扱われる可能性があります。これも undefined behavior の一種であり、間違った処理ではありませんが、やはり診 断メッセージを出すのが適当です。 * ANSI C 2.1.1.2 (C90 5.1.1.2) Translation phases 翻訳フェーズ C99 5.1.1.2 Translation phases [u.1.2] ソースファイルが で終わり ソースファイルが sequence で終わっているのは、 undefined behavior を引き起こします。* * ANSI C 2.1.1.2 (C90 5.1.1.2) Translation phases 翻訳フェーズ C99 5.1.1.2 Translation phases [u.1.3] ソースファイルがコメントの途中で終わっている ソースファイルがコメントの途中で終わっているのは、undefined behavior を引き起こします。これは実際にはコメントの閉じ忘れまたはコメントのネスト です。* * ANSI C 2.1.1.2 (C90 5.1.1.2) Translation phases 翻訳フェーズ C99 5.1.1.2 Translation phases [u.1.4] ソースファイルがマクロ呼び出しの途中で終わっている ソースファイルが完結しないマクロ呼び出しで終わっているのは、undefined だと考えられます。* これはマクロの引数を閉じるカッコを忘れた場合などに発生するもので、診断 メッセージは重要です。 * ANSI C 3.8.3.4 (C90 6.8.3.4) Rescanning and further replacement 再 走査と再置換え C99 6.10.3.4 Rescanning and further replacement [u.1.5] 引用でないところに不正な character がある 文字列リテラル、文字定数、header-name、コメント以外のところに書ける文 字はごく限られています。大文字と小文字の alphabet、数字、29種の記号、 6種の white space characters です。ソースですから当然のことです。* ここでは、white space 以外のコントロールコードがあった場合のテストをし ます。コントロールコードはたとえ文字列リテラル等の中であっても不正だと考 えられますが、これはコンパイラ本体でチェックされるはずであり、また一般に 文字セットには locale-specific ないし implementation-defined な面が多く、 範囲が必ずしも明確ではないので、ここではテストしません。また、漢字も上記 以外の場所では undefined と考えられますが、同様の理由でテストしません。 * ANSI C 2.2.1 (C90 5.2.1) Character sets 文字集合 C99 5.2.1 Character sets C99 では UCN が追加された。 [u.1.6] コントロール行中に [VT][FF] がある White space characters であっても、# で始まる preprocessing directive 行では [SPACE][TAB] 以外があると violation of constraint となります。し かし、これは translation phase 4 での話で、その前に phase 3 でそれらをそ の前後の 以外の sequence of white spaces とともに one space に 圧縮することもでき、その場合は violation は発生しません。* 規定はそうですが、やはりこれには診断メッセージを出すのが適当でしょう。 このテストは undefined behavior のテストではありませんが、他に分類しがた い特殊なものなので、便宜上ここに入れてあります。 * ANSI C 2.1.1.2 (C90 5.1.1.2) Translation phases 翻訳フェーズ C99 5.1.1.2 Translation phases ANSI C 3.8 (C90 6.8) Preprocessing directives 前処理指令 -- Constraints 制約 C99 6.10 Preprocessing directives -- Constraints [u.1.7] 引用の中に不正な multi-byte character sequence 文字列リテラル、文字定数、header-name、コメントの中であっても、multi- byte character として認められない sequence があると undefined です。 Multi-byte character の第一バイトの次のバイトが第二バイトとして使えない ものである場合です。* 配点 : 7。7種の各 encoding について 1 点。なお、m.34 の説明を参照の こと。 * ANSI C 2.2.1.2 (C90 5.2.1.2) Multibyte characters 多バイト文字 C99 5.2.1.2 Multibyte characters [u.1.8] 論理行が文字定数の途中で終わっている 文字定数という pp-token はその論理行で完結していなければなりません。論 理行中に対応する ' がない ' があると undefined です。* #error 行には任意のメッセージを書くことができますが、それも形としては pp-token の並びになっていないといけないので、単独の apostrophe はいけま せん。このサンプルでは、コメントのつもりのところも文字定数の終わりの ' をサーチするために食われてしまうでしょう。 * ANSI C 3.1 (C90 6.1) Lexical elements 字句要素 -- Semantics 意味規 則 C99 6.4 Lexical elements -- Semantics [u.1.9] 論理行が文字列リテラルの途中で終わっている 文字列リテラルもその論理行で完結していなければなりません。単独の " は undefined です。* かつては UNIX 系の多くの処理系では、行をまたぐ文字列リテラルというもの が認められていたようです。いまだにそれをあてにしたソースが一部に見られま す。 * ANSI C 3.1 (C90 6.1) Lexical elements 字句要素 -- Semantics 意味規 則 C99 6.4 Lexical elements -- Semantics [u.1.10] 論理行が header name の途中で終わっている #include の論理行で完結しない header-name も undefined です。* * ANSI C 3.8.2 (C90 6.8.2) Source file inclusion ソースファイル取込み -- Semantics 意味規則 C99 6.10.2 Source file inclusion [u.1.11] Header name 中に ', ", /*, \ がある Header-name という pp-token の中に ', /*, \ があると undefined です。<, > で囲まれる header-name の中に " がある場合も同様です(文字列リテラルの 形の header-name では、初めから " は pp-token の終わりになってしまうので、 エラー)。* これらは \ 以外はいずれも、文字定数、文字列リテラル、コメントと紛らわ しく、どちらにも解釈できるからです。 また、\ は escape sequence と紛らわしく、header-name には escape sequence は存在しないものの、これが header-name だとわかるのは transla- tion phase 3 での tokenization が終わって phase 4 になってからであるので、 やはり処理系は区別に困るのです。Escape sequence が処理されるのは phase 6 ですが、phase 3 でも \" を文字列リテラルの終わりではなく escape sequence と解釈するために escape sequence を認識することが必要だからです。 しかし、\ は MS-DOS 等のOSでは正規の path-delimiter で、それらのOS 上の処理系は当然、これを正しい文字として扱います(文字列リテラルの形の header-name でその最後の文字が \ である場合だけは別として)。Undefined behavior をもたらすのは多くの場合は間違ったプログラムですが、常にそうだ というわけではありません。しかし、/ ですむところをわざわざ \ と書くのは、 portability の上で感心しません。処理系はウォーニングを出すことが望まれま す。他のOSではファイルをオープンしようとしてエラーになるでしょうから、 必ずしもプリプロセスの tokenization で診断する必要はありません。 * ANSI C 3.1.7 (C90 6.1.7) Header names ヘッダ名 -- Semantics 意味規 則 C99 6.4.7 Header names -- Semantics [u.1.12] #include の引数が header name でない #include 行の引数が header-name でないのは undefined です。すなわち、 文字列リテラルの形でもなく、<, > に囲まれたものでもなく、そのどちらかに 展開されるマクロでもない場合です。 * ANSI C 3.8.2 (C90 6.8.2) Source file inclusion ソースファイル取込み -- Semantics 意味規則 C99 6.10.2 Source file inclusion -- Semantics [u.1.13] #include の引数に余計なトークンがある #include 行の引数は header-name 1つだけです。それ以外の余計な pp- token があると undefined です。 * ANSI C 3.8.2 (C90 6.8.2) Source file inclusion ソースファイル取込み -- Semantics 意味規則 C99 6.10.2 Source file inclusion -- Semantics [u.1.14] #line の引数に行番号がない #line の引数に行番号がないのは undefined です(ファィル名は optional。 行番号は第一引数でなければならない)。* * 以下、[u.1.18] までの典拠はいずれも同じ。 ANSI C 3.8.4 (C90 6.8.4) Line control 行制御 -- Semantics 意味規則 C99 6.10.4 Line control -- Semantics [u.1.15] #line のファイル名引数が文字列リテラルでない #line の第二引数であるファイル名は文字列リテラルでなければなりません。 これがワイド文字列リララルであった場合だけが violation of constraint で、他の #line の間違いはすべて undefined というのは、バランスを欠いた規 定です。 [u.1.16] #line の引数に余計なトークンがある #line 行に3つ以上の引数があるのは undefined です。 [u.1.17] #line の行番号の引数が [1, 32767] の範囲にない C90 では、#line の行番号引数は [1, 32767] の範囲になければなりません。 それ以外では undefined です。 このサンプルでは、#line での指定はこの範囲にあるものの、その後でソース の行番号がこれを超えてしまったという場合のテストもしています。処理系によ っては、ここで行番号が黙って wrap round したりしますが、やはりウォーニン グを出すのが適当でしょう。 配点 : 2。3つのサンプルのうち1つまたは2つ診断できれば 1 点。 [u.line] C99: #line の行番号の引数が範囲外 C99 では、この範囲は [1, 2147483647] です。 配点 : 2。3つのサンプルのうち1つまたは2つ診断できれば 1 点。 [u.1.18] #line の行番号の引数が10進数でない #line の行番号引数は10進数でなければなりません。16進数などは unde- fined です。 [u.1.19] #if 行中に defined に展開されるマクロがある defined が演算子でありながら identifier と紛らわしい形をしていることは、 さまざまな問題を引き起こします。Translation phase 3 ではいったん identifier として tokenize され、phase 4 でこれが #if 行中にあった場合に 限って演算子として認識されるので、#define 行で defined に展開されるマク ロを定義することも、ありえないことではありませんそして、#if 行に実際にこ のマクロが現れた場合は、undefined です。それを展開した結果が演算子として 扱われる保証はありません。* defined という名前のマクロを定義することはそれ自体が undefined ですが ([u.1.21] 参照)、実際にそういう例は見掛けません。しかし、置換リスト中 に defined というトークンのあるマクロ定義は、時に見掛けます。処理系によ っては、これについて特殊な処理をして合法的なものとして扱うものもあります が、論理的な仕様ではありません。 配点 : 2。2つのサンプルのうち1つだけ診断できた場合は 1 点。 * ANSI C 3.8.1 (C90 6.8.1) Conditional inclusion 条件付き取込み -- Semantics 意味規則 C99 6.10.1 Conditional inclusion -- Semantics [u.1.20] #undef の引数が defined, __LINE__, etc. である #undef の引数が defined, __LINE__, __FILE__, __DATE__, __TIME__, __STDC__, __STDC_VERSION__ であると undefined です。*1, *2, *3 *1 ANSI C 3.8.8 (C90 6.8.8) Predefined macro names あらかじめ定義され たマクロ名 C99 6.10.8 Predefined macro names *2 Amendment 1 / 3.3 Version macro *3 C99 では、__STDC_HOSTED__, __STDC_ISO_10646__, __STDC_IEC_559__, __STDC_IEC_559_COMPLEX が追加された。 [u.1.21] #define のマクロ名が defined, __LINE__, etc. #define で定義するマクロ名が defined, __LINE__, __FILE__, __DATE__, __TIME__, __STDC__, __STDC_VERSION__ であると undefined です。*1, *2, *3 配点 : 2。3つのサンプルのうち2つ診断できた場合は 1 点。 *1 ANSI C 3.8.8 (C90 6.8.8) Predefined macro names あらかじめ定義され たマクロ名 *2 Amendment 1 / 3.3 Version macro *3 C99 では、__STDC_HOSTED__, __STDC_ISO_10646__, __STDC_IEC_559__, __STDC_IEC_559_COMPLEX が追加された。 [u.1.22] ## 演算子によって不正な pp-token が生成された ## 演算子によって pp-token が連結された結果は有効な(単一の)pp-token にならなければなりません。そうでない場合は undefined です。* このサンプルでは pp-number という Standard C で新しく規定された pp- token を題材にしています。 * ANSI C 3.8.3.3 (C90 6.8.3.3) The ## operator ##演算子 -- Semantics 意味規則 C99 6.10.3.3 The ## operator -- Semantics [u.concat] C99: ## 演算子によって不正な pp-token が生成された // は C99, C++ ではコメントマークですが、これは pp-token ではありませ ん。## 演算子によってこの sequence を生成しようとしても、その結果は undefined です。 そもそもコメントはマクロが定義されたり展開されたりする前に a space に 変換されているので、マクロによってコメントを生成することはできません。 [u.1.23] # 演算子によって不正な pp-token が生成された # 演算子による置換の結果は有効な(単一の)文字列リテラルにならなければ なりません。そうならなかった場合は undefined です。 これはめったに発生することのない問題です。このサンプルでわかるように、 \ がリテラルの外にあるという奇妙な場合の、そのまたさらに特殊な場合に限ら れます。このサンプルも、プリプロセスで診断されなくてもコンパイルフェーズ で診断されるでしょうし、それで十分です。しかし、処理系がダウンしたり、黙 って通してしまったりするのはいけません。 * ANSI C 3.8.3.2 (C90 6.8.3.2) The # operator #演算子 -- Semantics 意 味規則 C99 6.10.3.2 The # operator -- Semantics [u.1.24] マクロ呼び出しに空の引数がある 関数様マクロの呼び出しに空の引数があるのは C90 では undefined です。*1 空の引数を0個のトークンと解釈して reasonable なマクロ展開を行うことは、 C99 で合法となったように十分可能で意味のあることです。*2 これは undefined な規定に処理系が有意義な定義を与えることのできる1つ の例です。しかし、そういう処理系であっても、少なくとも pre-C99 ではこれ には移植性がないので、ウォーニングを出すのが適当でしょう。 配点 : 2。5つのサンプルのうち3つまたは4つ診断できた場合は 1 点。 *1 ANSI C 3.8.3 (C90 6.8.3) Macro replacement マクロ置換え -- Semantics 意味規則 *2 C99 6.10.3 Macro replacement -- Semantics [u.1.25] マクロ呼び出しにコントロール行類似の引数がある 関数様マクロの呼び出しは複数の論理行にまたがることができます。したがっ て、引数の中に preprocessing directive と紛らわしい行が含まれることがあ りえますが、その結果は undefined です。* こうした「引数」は、スキップされる #if group の中にある場合は pre- processing directive 行と解釈されるでしょう。 * ANSI C 3.8.3 (C90 6.8.3) Macro replacement マクロ置換え -- Semantics 意味規則 C99 6.10.3 Macro replacement -- Semantics [u.1.26] Function-like マクロの名前で終わるマクロ展開 C90 では、マクロ展開の結果が function-like マクロの名前で終わるのは undefined とされています。後で追加された解釈ですが、意味不明なものです。 [1.7.6] を参照してください。 この Validation Suite では V.1.2 まではこれも一応テストに含めていまし たが、V.1.3 から削除しました。 * ISO/IEC 9899:1990 / Corrigendum 1 C99 ではこの規定は削除され、複雑な規定が追加されている。 [u.1.27] 無効なディレクティブ 行の最初の pp-token が # でその後にさらに pp-token がある場合は、通常 は # の次は preprocessing directive でなければなりません。# だけの行はな ぜか認められています。*1 しかし、行頭の # に無効なディレクティブやその他の pp-token が続くこと は、プリプロセスの violation of syntax or constraint ではありません。な ぜなら、[u.1.25] でわかるように、# で始まる行がすべてプリプロセスディレ クティブ行でなければならないわけではないからです。どれがプリプロセスディ レクティブ行であるのかは、文脈によって決まるものなのです。 規格書はこれを undefined とも明記していませんが、「規定されていない」 という意味でこれも undefined の一種です。処理系は何らかの診断メッセージ を出すことが望まれます。ただし、これは必ずしもプリプロセスが診断する必要 はありません。プリプロセスがこの行をそのまま出力すれば、コンパイルフェー ズでエラーになるはずですから、それでもかまいません。プリプロセスが # ifdefined を #if defined と解釈してエラーにならずじまいなどということさ えなければ、危険はありません。 C99 では # non-directive という意味不明なものが追加されましたが、その 内容は何も規定されておらず、事実上 undefined であると言えます。*2 *1 ANSI C 3.8 (C90 6.8) Preprocessing directives 前処理指令 -- Syntax 構文規則 *2 C99 6.10 Preprocessing directives -- Syntax [u.1.28] directive 名にマクロは使えない # で始まる行がプリプロセスディレクティブであるためには、次の pp-token としては directive 名しか許されません。Directive 名は決してマクロ展開さ れません。 # の次に directive 名ではない identifier が来て、それがマクロ名であっ た場合は、存在しないディレクティブと診断するか、通常のテキストとみなして マクロを展開して出力するかの、2つの処理がありえます。後者でも何らかの診 断が望まれます。後者ではコンパイルフェーズでエラーになるはずですから、そ れでもかまいません。マクロ展開したものをさらに「正しい」プリプロセスディ レクティブとして処理することだけは、あっては困ります。 [u.2.1] #if 式中に未定義の character escape sequence がある 文字列リテラルまたは文字定数中の character escape sequence は \', \", \?, \\, \a, \b, \f, \n, \r, \t, \v だけが規定されています。それ以外の \ で始まる character sequence は undefined です。ことに \ に小文字が続く sequence は、将来 escape sequence を追加するために予約されています。* これらの診断の多くはコンパイルフェーズにまかせればすみますが、#if 式の 文字定数中にこれがあった場合だけは、プリプロセス以外に診断する者はいませ ん。 * ANSI C 3.1.3.4 (C90 6.1.3.4) Character constants 文字定数 -- Description 補足規定 C99 6.4.4.4 Character constants -- Description ANSI C 3.9.2 (C90 6.9.2) Character escape sequences 文字拡張表記 C99 6.11.4 Character escape sequences C99 では \uxxxx, \Uxxxxxxxx の形の UCN (universal-character-name) という escape sequence が追加された。 [u.2.2] #if 式中にシフトカウントが不正なビットシフト演算 整数型のビットシフト演算では、右 operand(シフトカウント)が負値であっ たり左 operand の型のビット数以上であったりした場合は undefined です。* #if 行にこれがあった場合はプリプロセスが診断すべきでしょう。 * ANSI C 3.3.7 (C90 6.3.7) Bitwise shift operators ビット単位のシフト 演算子 -- Semantics 意味規則 C99 6.5.7 Bitwise shift operators -- Semantics [3.3] Unspecified behavior Standard C には unspecified という規定(「規定しない」という規定)もあ ります。これは、正しいプログラムであっても、その処理方法は規定しない、処 理系は処理方法をドキュメントに書く必要もない、というものです。 この例はあまり多くはありません。中でも、処理方法によって結果に違いが出 るのは、きわめて特殊な場合だけです。しかし、特殊であっても、結果に違いの 出る恐れのあるものに対してはウォーニングを出すのが望ましいと思われます。 Unspecified な動作に依存するプログラムの結果は undefined です。 プリプロセスでは unspecified で、しかもその処理によって結果が違ってく るのは、次の2点です。この2つのテストではそれぞれ、不正な pp-token が生 成されて診断メッセージが出されるか、それとも移植性がないというウォーニン グが出されるか、どちらでも 2 点を与えることにします。また後者の場合は、 マクロ定義時でも展開時でもどちらでも良いことにします。 なお、そのほかに #if 式での演算の評価順序も unspecified ですが、#if 式 はそれによって結果が変わることはないので、テストに含めていません。 [s.1.1] # 演算子と ## 演算子の評価順序は指定されていない 1つのマクロ定義中に # 演算子と ## 演算子の双方があった場合、そのどち らが先に評価されるかは規定されていません。* このサンプルは # と ## のどちらが先に評価されるかで結果が違ってくる例 です。しかも、## が先に評価されると # は演算子ではなく単なる pp-token と して扱われ、連結の結果は不正な pp-token が生成されてしまいます。こういう マクロには移植性がないので、処理系はウォーニングを出すのが適当です。 配点 : 2。 * ANSI C 3.8.3.2 (C90 6.8.3.2) The # operator #演算子 -- Semantics 意 味規則 C99 6.10.3.2 The # operator -- Semantics [s.1.2] 複数の ## 演算子の評価順序は指定されていない 1つのマクロ定義中に複数の ## 演算子がある場合、その評価順序は規定され ていません。* このサンプルでは、評価する順序によっては途中で不正な pp-token が生成さ れます。こういうマクロには移植性がないので、処理系はウォーニングを出すの が適当です。 配点 : 2。 * ANSI C 3.8.3.3 (C90 6.8.3.3) The ## operator ##演算子 -- Semantics 意味規則 C99 6.10.3.3 The ## operator -- Semantics [3.4] ウォーニングの望ましいその他のケース Undefined, unspecified のほかにも、処理系がウォーニングを出すことが望 ましいケースがいくつかあります。それらをここに集めました。 w.1.*, w.2.* は規格ではまったく正しいプログラムですが、実際には何らか の間違いである場合が多く、診断が重要なものです。w.1.* はプリプロセスに固 有の問題で、w.2.* はコンパイルフェーズでの演算とも共通した問題の #if 式 版です。 w.3.* は translation limits という implementation-defined な側面を持つ 規定に関連したものです。規格で保証されている最小限の値を超える transla- tion limits を実装することは、処理系の品質を上げることであるとも言えます が、しかし他方で、それに依存するプログラムは移植性が制限されてしまうとい う問題も発生します。したがって、最小限の値を超える translation limits を 実装している処理系では、そのことを利用するプログラムに対してウォーニング を出すのが望ましいと考えられます。 以下のテストでは、適切な診断メッセージを出せば合格とします。w.3.* は処 理系の translation limits が規格の最小値と一致していて、そのためこれらの サンプルがエラーとなることも許容することにします。最小値に満たないために エラーとなるのは不合格です(満たしているかどうかは n.37.* でわかる)。 [w.1.1] コメント中に /* がある コメントがネストされていたりコメントマークの片方がなかったりするソース のタイピングミスはよくあります。そのうち、/* /* */ */ というコメントのネ スト、および */ だけがある場合は */ という sequence はC言語にはないので、 コンパイルフェーズで必ずエラーになります。しかし、*/ が脱落している場合 は次のコメントの終わりまでがコメントと解釈されるので、エラーにならないこ とがあります。これは危険な間違いであり、プリプロセスがウォーニングを出す ことが大事です。コンパイルフェーズで何らかのエラーになった場合でも、その 時には原因はわからなくなっています。 配点 : 4。 [w.1.2] マクロの再走査が後続のトークン列を取り込む マクロの再走査で置換リストの後ろのトークン列が取り込まれる場合があるの は、K&R 1st. では暗黙の仕様であり、Standard C では公認の仕様ですが、この 事態を引き起こすのは尋常なマクロではありません。ことに置換リストが他の function-like マクロ呼び出しの前半部分を構成するのは、きわめて異常なマク ロです。実際にはマクロ定義の間違いである可能性が大きく、ウォーニングを出 すことが望まれます。Object-like マクロが function-like マクロの名前に展 開されるものは実際にも時々見られますが、readability の悪い書き方です。 これについては規格のほうに問題があります。再走査が置換リスト内で完結し ない場合はエラー(violation of constraint)とすべきであると思われます ([1.7.6] 参照)。 配点 : 4。2つのサンプルのうち1つだけ診断できた場合は 2 点。 [w.2.1] #if 式の通常の算術変換で負数が符号無し数に変換 符号付き整数と符号無し整数との混合演算に際しては「通常の算術変換」が行 われ、符号付きが符号無しに変換されます。元の符号付き整数が正数であった場 合は値は変わりませんが、負数であった場合はこれが大きな正数に変換されます。 これはエラーではありませんが、異常なものであり、何かの間違いである可能性 があります。処理系がウォーニングを出すことが望まれます。プリプロセスでは この現象は #if 式に現れます。 配点 : 2。 [w.2.2] #if 式の符合なし演算が wrap round 符号無し演算の結果が範囲外となった場合は wrap round することになってい るので、エラーにはなりませんが、間違いの可能性があるのでウォーニングを出 すのが望ましいでしょう。 配点 : 1。 [w.3.1] 31 個を超えるマクロパラメータ [w.3.2] 31 個を超えるマクロ引数 w.3.? はいずれも C90 の translation limits に関するテストです。内容は 自明のことで、説明は要しないでしょう。n.37.* と比べてみてください。 配点 : 1。3.1, 3.2 は2つのどちらかを診断すればすむ。 [w.3.3] 31 バイトを超える長さの identifier 配点 : 1。 [w.3.4] 8 レベルを超える #if (#ifdef, #ifndef) のネスト 配点 : 1。 [w.3.5] 8 レベルを超える #include のネスト 配点 : 1。 [w.3.6] #if 式中の 32 レベルを超える副式のネスト 配点 : 1。 [w.3.7] 509 バイトを超える長さの文字列リテラル 配点 : 1。これはプリプロセッサが診断しなくても、コンパイラ本体が診断 すれば良しとします。 [w.3.8] 509 バイトを超える長さの論理行 配点 : 1。 [w.3.9] 1024 個を超えるマクロ定義 これだけは n.37.9 と同じものになっています。Translation limits の規定 では、これが最もおおまかなものなのです。1024 個のマクロに組み込みマクロ も数えるかどうか、標準ヘッダ中で定義されるマクロも数えるかどうかで、数が 違ってきます。このサンプルでテストされるマクロはヘッダファイルの中では 1024 個目ですが、warns.t, warns.c の中にはそれ以前に定義されているマクロ がいくつかあるので、このマクロはどちらにしても 1024 個目を超えることにな ります。処理系は適当なところでウォーニングを出すことが期待されます。 配点 : 1。 [w.tlimit] C99 の translation limits C99 では translation limits が大幅に拡大されました。これを超える仕様を 持つ処理系でも、portability のためには規定を超えるソースに対してはウォー ニングを出すことが望まれます。 配点 : 8。各項目について、1 点ずつ。ただし、3.1, 3.2 はどちらかを診断 できればすむ。3.7 はコンパイラ本体が診断するなら、それでも可。 テスト用サンプルは test-l ディレクトリにある。なお、l_37_8.c はプリプ ロセスはできてもコンパイルはできない疑似ソースである。 [w.3.1L] 127 個を超えるマクロパラメータ [w.3.2L] 127 個を超えるマクロ引数 [w.3.3L] 63 バイトを超える長さの identifier [w.3.4L] 63 レベルを超える #if (#ifdef, #ifndef) のネスト [w.3.5L] 15 レベルを超える #include のネスト [w.3.6L] #if 式中の 63 レベルを超える副式のネスト [w.3.7L] 4095 バイトを超える長さの文字列リテラル [w.3.8L] 4095 バイトを越える長さの論理行 [w.3.9L] 4095 個を超えるマクロ定義 [3.5] その他の各種品質 以下には、処理系の使いやすさ等の品質に関する事項を集めています。q.1.1 以外はサンプルプログラムでテストすることのできないものです。 q.1.* は動作に関するものです。 q.2.* はオプションや拡張機能に関するものです。 q.3.* は稼働しうるシステムやシステム上での効率に関するものです。 q.4.* はドキュメントに関するものです。 これらの中には、かなり主観的な評価にたよらざるをえないものもあります。 また客観的に評価できるものの、その尺度を明示できないものもあります。[5.2] の例を参考に、適当に採点してください。 [3.5.1] 動作に関する品質 [q.1.1] 規定を上回る translation limits Translation limits については、規格では最低限の仕様がゆるやかに規定さ れていますが、実際の処理系はこれを上回る仕様を持っていることが望まれます。 ことに、#if のネストレベル、#include のネストレベルについては、C90 の要 求は低すぎると考えられます。 C99 では translation limits は大幅に引き上げられています。また、 identifier の長さについては、255 バイト未満に制限しているのは obsolescent feature(廃止予定の規定)とされています。 q.* の項目の中ではこれだけがテスト用サンプルが用意されています。test-l ディレクトリにある l_37_?.t, l_37_?.c で、それぞれ次のような translation limits をテストします。これは C99 のものをさらに上回っています(しかし、 C++ Standard のガイドラインとしての translation limits の値は下回ってい る)。 37.1L : マクロ定義中のパラメータの数 : 255 37.2L : マクロ呼び出しの引数の数 : 255 37.3L : identifier の長さ : 255 bytes 37.4L : #if のネストレベル : 255 37.5L : #include のネストレベル : 127 37.6L : #if 式の副式のネストレベル : 255 37.7L : 文字列リテラルの長さ : 8191 bytes 37.8L : ソースの論理行の長さ : 16383 bytes 37.9L : マクロ定義の数 : 8191 l_37_8.c はコンパイルしても実行プログラムにはなりません。プリプロセス するだけであれば結果を見ればわかりますが、コンパイルする場合は cc -c l_37_8.c 等としてオブジェクトファイルを作ってください。プリプロセスが成 功すれば、コンパイラ本体がどれだけの長さの行を受け入れることができるかが わかります。 配点 : 9。9種のサンプルについて各 1 点。コンパイラ本体のテストは含ま ない。 [q.1.2] 診断メッセージの的確さ 診断メッセージが出ることは出るものの、わかりにくかったり、おおまかすぎ たり、問題の個所がわからなかったりするものもあります。ある種類の診断メッ セージは詳しく出るが、別の種類の診断はピントがズレているという場合もあり ます。診断メッセージは単に "syntax error" などとするのではなく、なぜ間違 いであるのかを示してもらいたいものです。個所は行を示して、さらに問題のト ークンを指摘してもらいたいものです。 また、#if section の対応関係のエラーでは、対応すべき行を教えてくれない と、どこにエラーの元があるのかがわかりません。 同一のエラーに対していくつもの診断メッセージが重複して出るのは、好まし くありません。 配点 : 10。 [q.1.3] 行番号表示の正確さ プリプロセッサがコンパイラ本体に渡す行番号がズレてしまうのは困ります。 これは診断メッセージに現れるものですが、便宜上独立した項目にしておきます。 この採点は、これまでのサンプルプログラムのテストで行番号が正確に表示され たかどうかで行います。中には行番号情報を出力しないプリプロセッサもありま すが、それは論外です。 配点 : 4。 [q.1.4] 暴走・中断 配点 : 20。この Validation Suite のどれかのサンプルで暴走したり、中断 すべきでない処理を中断した処理系には、次のように減点する。「暴走」という のは、^C で中断せざるをえなくなったり、リセットせざるをえなくなったり、 ファイルシステムに不整合を残したりするもので、「中断」というのは、これら の被害はないものの、処理の途中で終了してしまうものを指す。 1.n_std.c(strictly conforming program)で暴走したら 0 点。 2.n_std.c で処理を中断したら 10 点(中断した個所の後の部分をさらにテ ストして暴走したら、「暴走」に分類)。 3.それ以外のどれかのサンプルで暴走したら 10 点。 [3.5.2] オプションと拡張機能 [q.2.1] Include directory の指定 標準ヘッダファイルの置かれるいわゆる include directory は最も単純な場 合は1個所に固定されていますが、複数存在する場合もしばしばあり、ユーザが 指定しなければならない場合もあります。また、ユーザレベルのヘッダファイル については、カレントディレクトリにある場合は問題ありませんが、別のディレ クトリにあって、それがさらに別のヘッダファイルを include する場合は、処 理系によってディレクトリをサーチする規則が異なります。どちらにしても、 include directory はオプションや環境変数によってユーザが明示的に指定する こともできないと不便です(-I オプションを使う処理系が多い)。また、複数 のディレクトリを順にサーチする場合は、システムで既定のディレクトリを外す オプションもないと不便です。サーチする規則そのものを変更するオプションに も、存在理由があります。 配点 : 4。 [q.2.2] マクロ定義オプション オブジェクト様マクロをソース中でなくコンパイル時に定義できるオプション は、有用なものです(-D オプションを使う処理系が多い)。これによって、同 一のソースから違った仕様のオブジェクトを作ったり、違ったシステムでコンパ イルしたりすることができます。置換リストを省略した場合は 1 に定義する処 理系と0個のトークンに定義する処理系とがあり、使う時はドキュメントを確か めなければなりません。 引数つきマクロの定義もオプションでできる処理系もあります。 Trigraph を実装している処理系でも、このオプションでそれが使えないのは 片手落ちです(しかし、この際、そこまで評点の対象にはしない)。 配点 : 4。 [q.2.3] マクロ取り消しオプション 処理系固有の組み込みマクロを取り消すオプションも、あったほうが良いでし ょう。次のような種類があります。 1.-U といったオプションで1つのマクロを取り消すもの。 2.処理系固有の組み込みマクロを一括して無効にするオプション。 3.Standard C で禁止されている組み込みマクロ(_ で始まらない unix 等) だけを一括して無効にするオプション。 配点 : 2。この1または2のオプションがあれば 2 点。3は d.1.5 ですで に評価しているので、ここでは対象としない。 [q.2.4] Trigraphs オプション Trigraphs は必要な環境では常用するのでしょうが、多くの環境では必要がな いのでほとんど使いません。これはコンパイル時のオプションで有効にしたり無 効にしたりできるほうが、良さそうです。 配点 : 2。 [q.2.5] Digraphs オプション Digraphs も trigraphs と同様に、コンパイル時のオプションで有効にしたり 無効にしたりできるほうが、良いでしょう。 配点 : 2。 [q.2.6] ウォーニング指定オプション Violation of syntax rule or constraint ではないものに対するウォーニン グは、なるべく多面的に出してくれたほうが役に立ちますが、場合によってはう るさいこともあります。ウォーニングのレベルを指定するオプション、または種 類ごとにウォーニングを有効にしたり無効にしたりするオプションは、欲しいも のです。 配点 : 4。 [q.2.7] その他のオプション プリプロセスにはそのほかにも有用なオプションがいくつかありえます。やた らにオプションが多いのは煩雑ですが、ないと何かの時に不便なものもあります。 比較的よくあるのは、行番号情報を出力しないオプション(-P が多い)で、こ れはC言語以外の目的に使うものだと思われます。コメントを削除せずに出力す るものもあります。OSのコマンドプロセッサによっては、診断メッセージのリ ダイレクトを処理系のほうで実現する必要のある場合もあります。また、プリプ ロセスが独立したプログラムではない、いわゆる1パス・コンパイラでは、プリ プロセス後の出力を指定するオプションがぜひ欲しいところです。 C90 (C95), C99, C++ を区別するオプションは当然必要なものですが、さらに C99 と C++ の互換性を上げる(C++ のプリプロセスを C99 のものに近づける) オプションも有用でしょう。 Makefile を作成するための、ソースファイルの依存関係記述行を出力するオ プションを持っているプリプロセッサもあります。 配点 : 10。 [q.2.8] #pragma による拡張 Standard C では、処理系固有の directive はすべて #pragma の sub- directive として実現することになっています。プリプロセスはたいていの # pragma をそのままコンパイラに渡しますが、一部の #pragma は自分自身が処理 します。プリプロセスが処理する #pragma の例は多くはありません。 #pragma once と書いてあるヘッダファイルは何回 #include されても一度し か読まないというものがありますが、これは多重 include を避けるためだけで なく、処理速度を上げるために有効なものです。#pragma once を使わずに、ヘ ッダファイル全体が例えば #ifndef __STDIO_H #define __STDIO_H ... #endif といった皮でくるまれていると、自動的にこれを次回はアクセスしないようにす る処理系もあります。 ヘッダファイルを "pre-preprocess" して、多くのヘッダファイルを1本にま とめてしまうという使い方のできる #pragma directive もあります。すなわち、 #include で取り込まれるヘッダをいったんプリプロセスして出力し、そこに現 れた #define directive をまとめて出力に追加するという機能です。元ソース のコンパイルではこれを include すれば足ります。こうして pre-preprocess したヘッダファイルは、コメントと #if がなくなるのでサイズが小さくなり、 マクロ呼び出しもなくなります。アクセスするファイルも1つですみます。結果 としてプリプロセスが速くなります。 ヘッダの pre-compile という機能を持つ処理系もあります。これは主として C++ で巨大なヘッダファイルを処理するために考えられたもののようですが、 pre-compiled header のサイズが元のヘッダファイルの合計よりも大きくなる傾 向があり、少なくとも C では速度向上の効果はあまりないようです。Pre- compiled header の内容が compiler-proper の内部仕様に依存していて、ユー ザには見えない black box になるところも、難点です。 どちらにしても、以上はすべてプリプロセスを速くするためのもので、それ以 外の意味はありません。したがって、これらの機能はここでは評価せず、[q.3.1] で評価することにします。 Multi-byte character の encoding を指定する #pragma を持つ処理系もあり ます。プリプロセッサやコンパイラにソースの文字の encoding を伝える方法と しては、完全なものです。 プリプロセスをトレースしデバッグ情報を出力する #pragma を持つものもあ ります。通常のデバッガではプリプロセスのデバッグはできないので、これはプ リプロセッサでしかできない重要な機能です。この機能はオプションで指定する よりは #pragma を使うほうが、デバッグする個所を限定できるので使いやすく なります。 エラーやウォーニングのコントロールの指定のように、通常はコンパイル時オ プションで指定することを #pragma で実装している処理系もあります。#pragma は Standard C 準拠の処理系であれば portability の問題がないことと、ソー ス上の場所を特定して指定できるという長所がありますが、それを変更する時は ソースを書き替えることになるという短所もあります。実装するなら、コンパイ ル時オプションを実装した上でしてもらいたいものです。 それ以外の #pragma でプリプロセスで処理するものは、あまりありません。 ところで、#pragma sub-directive は implementation-defined であるため同 じ名前の sub-directive が処理系によって異なる意味を持つ恐れがあります。 名前の衝突を避ける工夫が望まれます。GNU C 3 では #pragma GCC poison とい うふうに GCC という名前で始まりますが、これは良い方法です。MCPP も V.2.5 からはこれをまねて #pragma MCPP debug というふうに MCPP という名前で始ま るようにしました。 配点 : 10。 [q.2.9] 拡張機能 拡張機能は #pragma で実装することになっていますが、それとは別にプリプ ロセスの新しい仕様の提案として #pragma ではないディレクティブが実装され る場合もあります。 Linux のシステムヘッダでは GNU C の #include_next が使われています。し かし、これはシステムヘッダが無用に複雑化していることによるもので、感心し たことではありません。GNU C / cpp にはそのほかにも #warning, #assert 等 々の規格外のディレクティブがありますが、いずれもあまり必要性の感じられな いものです。GNU C 3.2 でこれらのいくつかが obsolete とされたのは良い方向 だと思われます。 Wave というプリプロセッサではマクロ定義の有効範囲を指定するディレクテ ィブがあり、それに対応してマクロ呼び出しの記法にも C++ のスコープ指定と 似た記法を採用しています。実験としては興味のあるものです。 GNU C では標準モードのプリプロセッサのほかに traditional モードのもの があります。MCPP はコンパイル時の設定によって多様な仕様のプリプロセッサ が生成されるようになっています。こうした試みにはそれなりの意味があります。 配点 : 6。 [3.5.3] 実行効率等 [q.3.1] 処理速度 処理の正確さと診断の的確さとは最も重要なものですが、速度も速いにこした ことはありません。 速度向上のための #pragma やオプションも動員して、結果としてどういう速 度になるかを見ます。 配点 : 20。入力を出力にコピーするだけの、何もしないプログラムの速度を 20 点とし、それより相対的にどれだけ遅いかによって点を付ける。具体的な尺 度は [5.2] の例を参考。絶対的な速度はもちろんハードウェアによって違うの で、同程度のハードウェアを前提として比較する。また、同じプログラムを処理 しても、読み込まれる処理系の標準ヘッダの量によって処理時間が違ってくる。 MCPP を基準として比較するのが良い。 時間を測るには、UNIX 系では time コマンドを使う(bash, tcsh では組み込 みコマンドである)。DOS/Windows 系では、CygWIN が使えるなら、やはり bash に time コマンドがある。また、WindowsNT の「リソースキット」に TimeThis という同様のコマンドがある (*)。これらが使えない環境では、tool/clock_of. c をコンパイルして使う(かなり不正確であるが)。 * WindowsNT のリソースキットのプログラムには WindowsXP では使えるもの と使えないものとあるが、TimeThis は使えるようである。また、DOS の実 行プログラムに対しても、WindowsNT, WindowsXP 上では TimeThis を使う ことができる(DOS を起動するオーバーヘッドが大きいので、不正確になる が)。 [q.3.2] メモリ使用量 メモリ使用量は少ないに越したことはありません。ことにシステムの与えるメ モリ量に厳しい制限がある場合は、これは切実な問題となります。 プリプロセスはコンパイルの一部であるので、実際には処理系全体のメモリ使 用量が問題となります。プリプロセスが独立したプリプロセッサによって行われ る場合は、通常はコンパイラ本体のほうがメモリを食うので、プリプロセッサの メモリ使用量はあまり問題にならないでしょう。しかし、マクロ定義が多い場合 など、プリプロセッサのほうが out of memory になることもあります。メモリ 使用量にはプログラムの大きさばかりではなく、データエリアの使用量も含まれ ます。 配点 : 20。通常の使用で out of memory が発生するものは 10 点以上減点 する。データサイズを測定できない場合はプログラムサイズだけで評価する。 UNIX 系では file, ldd コマンドも参考にする。tcsh の time コマンドの出力 も参考になる。 [q.3.3] 移植性 プリプロセッサそのもののソースの移植性は、処理系の既存のプリプロセッサ と取り替えようとする時や、自分自身をアプデートしたりカスタマイズしたりし ようとする時に、問題となります。次のような点が評価の対象となるでしょう。 1.ソースが公開されているか(公開されていなければ 0 点)。 2.多くの処理系・OSに対応しているか。 3.移植できる処理系・OSの範囲はどうか。どういう条件が前提されている か。 4.移植しやすいソースか。 5.移植のためのドキュメントが整備されているか。 配点 : 20。しかし、私がソースを読んだのは1つ半くらいにすぎない。後は 眺めただけである。したがって、この採点はあてずっぽうである。 [3.5.4] ドキュメントの品質 [q.4.1] ドキュメントの品質 d.* では Standard C の「処理系定義事項」についてのドキュメントがあるか ないかだけを見ましたが、ここではドキュメントのその他の品質を評価します。 ドキュメントとしては処理系定義事項のほかに、最低限、次のものが必要でし ょう。 1.Standard C との異同。 2.オプションの仕様。 3.診断メッセージの意味。 そのほか、Standard C の部分を含めたプリプロセス全体の仕様の解説もあれ ば、それに越したことはありません。 これらについて、正確さ、読みやすさ、検索性、一覧性等が評価の対象となる でしょう。移植のためのドキュメントは q.3.3 の評価に含めます。 配点 : 10。 [3.6] C++ のプリプロセス C処理系が C++ 処理系といっしょに提供されることが多くなってきましたが、 その場合はプリプロセッサはCと C++ とで同じものが使われているようです。 確かに両者のプリプロセスはほとんど同じなので、わざわざ別のプリプロセッサ を用意する必要はないでしょう。しかし、両者はまったく同じではありません。 C++ Standard を C90 と比べると、C++ のプリプロセスは C90 のプリプロセ スに次の仕様を付け加えたものになっています。 1.Basic source character set に含まれない character は、translation phase 1 で \Uxxxxxxxx の形の Unicode の16進 sequence に変換する。そし て、これは translation phase 5 で実行時文字セットに再変換する。*1 2.// から行末までをコメントとする。*2 3.::, .*, ->* をそれぞれ1つの pp-token として扱う。*3 4.#if 式では true, false を boolean literal として、それぞれ 1, 0 と 評価する。*4 5.ISO C : Amendment 1 (1995) では標準ヘッダ でマクロとし て定義される 11 種の identifier 様 operator は、すべてマクロではなく token である(無意味な仕様)。(*3) 同様に、new, delete も operator であ る。*5 6.マクロ __cplusplus が 199711L に pre-define されている。*6 7.__STDC__ を定義するかどうか、定義するとすればどう定義するかは implementation-defined である(逆に C99 では __cplusplus を定義すると undefined である)。*6 8.Translation limits は次のように大幅に拡大されている。ただし、これ はガイドラインであり、要求ではない。処理系は translation limits をドキュ メントに明記しなければならない。*7 ソースの論理行の長さ : 65536 バイト 文字列リテラル、文字定数、header name の長さ : 65536 バイト Identifier の長さ : 1024 文字 #include のネスト : 256 レベル #if, #ifdef, #ifndef のネスト : 256 レベル #if 式のカッコのネスト : 256 レベル マクロのパラメータの数 : 256 個 定義できるマクロの数 : 65536 個 9.Header name は '.' の前の長さに(規格による)制限はなくなった。*8 C99 ではこのうち2だけが同じ仕様になっていますが、他は異なります。また、 C99 ではさらに、浮動小数点数の中の p+, P+ という sequence、identifier 中 に multi-byte character を使える実装の公認、可変引数マクロ、カラ引数の合 法化、 #pragma 行の引数のマクロ展開可、_Pragma() operator、#if 式の long long 以上での評価、隣接する wide-character-string-literal と character- string-literal の wide-character-string-literal としての連結等が加わって、 新たな相違が発生しています。UCN は C99 では translation phase 5 だけの規 定になりました。UCN に関する constraint も少し相違しています。 Translation limits も C99 では C90 よりは大幅に拡大されたものの、C++ Standard ほど極端ではなく、ここでも違いが出てきています。 さほど大きな違いではないとも言えますが、それでもこれだけ違うと、Cと C++ を同じプリプロセスですますわけにはいきません。Cについても、C90 (C95) と C99 とは同一のプリプロセスで間に合わせることはできません。 なお、C++ で __STDC__ を事前定義しているのは間違いのもとであり、好まし いことではありません。 __cplusplus については、これを -D オプションで定義する処理系もあります が、それではユーザ定義マクロの1つになってしまうので、不適切です。 ::, .*, ->* を1つの token として扱うかどうかは、プリプロセスではほと んど問題にならないことですが、正しく扱うに越したことはありません。 こういうことで、C90 (C95), C99, C++ の間でプリプロセッサを共通にするた めには、実行時オプションで三者を区別し、それに応じて上記諸点の処理を変え るというのが、まともな実装だと思われます。 なお、MCPP では上記の仕様のうち次の2点は、効用に比べて実装の負担が大 きすぎるため、対応していません。実用上はこれでほとんど問題ないと思われま すが。 1.Translation phase 1 での UCN への変換は実装していない。C++ Standard では、必ずしも UCN に変換しなくても、それと同じ結果になるなら良 いとされている。しかし、実用上はともかく厳密には、変換せずに常に同じ結果 になるはずはないのである。#if 式での UCN と multi-byte-character との文 字定数の比較を考えればわかる。# 演算子による文字列化でも厳密に言えば、問 題が生じる。*9 2.マクロの最大パラメータ数は 255 個までしか実装できない。 MCPP ではまた、-+ -V199901L オプションで C++ プリプロセッサとして起動 して __cplusplus を 199901L 以上の値にすると、[1.8] の1〜13については 8、9以外は C99 と同じ仕様に拡張されます(3はオプションなしでも同じで ある。2は MCPP コンパイル時の実装による)。 以下に、C90 のプリプロセス規定に付加される C++ 独自のプリプロセス規定 への適合性のテストを示します。 ファイル名が *.cc 等となっていないと C++ のソースとして認識しない処理 系では、*.cc 等にコピーしてテストします。 Translation limits については test-l ディレクトリにあるもの以上のサン プルは用意していません。C++ では translation limits はガイドラインにすぎ ないということもあり、ここでは評点の対象としません。また、header name の 長さは OS しだいなので、テストの対象としません。 *1 C++ 2.1 Phases of translation *2 C++ 2.7 Comments *3 C++ 2.12 Operators and punctuators *4 C++ 2.13.5 Boolean literals なお、C99 では で bool, true, false, __bool_true_false_are_defined をマクロとしてそれぞれ _Bool, 1, 0, 1 と定義することになっている。 *5 C++ 2.5 Alternative tokens *6 C++ 16.8 Predefined macro names *7 C++ Annex B Implementation quantities *8 C++ 16.2 Source file inclusion C90 6.8.2 では、header name の '.' から左には6文字までしか保証され ていなかった。C99 6.10.2 では8文字である。しかし、C++ ではこの制限 が削除されている。 *9 C99 では、# 演算子によって UCN が文字列化された場合、\ を重ねるか どうかは implementation-defined である。C++ にはこの規定はない。 重ねた場合は、その UCN はもはや multi-byte character に戻らないので、 重ねないほうが良い実装である。しかし、C++ の規定では、重ねないのは 「間違った」実装になってしまう。 [3.5.n.ucn1] UCN の認識 配点 : 4。 [3.5.n.cnvucn] Multi-byte character の UCN への変換 配点 : 2。 [3.5.n.dslcom] // コメント 配点 : 4。 [3.5.n.bool] true, false は boolean literal 配点 : 2。 [3.5.n.token1] ::, .*, ->* はトークン 配点 : 2。テストが一見成功したようでも、Cで処理しても同じように、何 のウォーニングも出ずにトークン連結が「成功」してしまうのでは、不可。 [3.5.n.token2] Operator の代替トークン 配点 : 2。 [3.5.n.cplus] 事前定義マクロ __cplusplus 配点 : 4。__cplusplus < 199711L の場合は 2 点。 [3.5.e.operat] identifier-like operator はマクロ名に使えない 配点 : 2。2つともウォーニング等の診断メッセージが出れば 2 点。 [3.5.u.cplus] #define, #undef __cplusplus 配点 : 1。ウォーニング等の診断メッセージが出れば 1 点。 [3.5.d.tlimit] Translation limits のドキュメント 配点 : 2。 ☆ 4.Cプリプロセスの周辺 ☆ Cプリプロセッサの規格準拠度や品質の問題とは別に、実際にプリプロセッサ を使う時に出会う周辺の問題を、以下に取り上げてみます。 [4.1] 標準ヘッダ この Validation Suite のサンプルでは、いくつかの標準ヘッダを include しています。それらのヘッダが正しく書かれていないと、プリプロセッサそのも ののテストが正確にできません。 以下に、標準ヘッダの実装で問題の生じやすいところを見ていきます。 [4.1.1] 一般的規約 標準ヘッダは規定されている関数宣言や型定義・マクロ定義をすべて含んでい なければならないのはもちろんですが、さらに次のような条件を満たさなければ ならないことになっています。 1.規定されておらずかつ予約されてもいない identifier を宣言したり定義 したりしてはいけない。宣言できる範囲は標準ヘッダによって決まっている(重 複する、あるいは共通する範囲もある)。*1 2.したがって、1つの標準ヘッダが別の標準ヘッダを include するのは通 常はいけない。 3.複数の標準ヘッダをどのような順序で include しても、同じ結果になら なければならない。*2 4.同じ標準ヘッダを複数回 include しても、 以外は同じ結果に ならなければならない。*2 5.整数定数に展開される object-like マクロとして規定されているものは すべて #if 式でなければならない。*3 予約される identifier の範囲は規定されており、それ以外の identifier は ユーザに解放しておかなければなりません。'_' 1つまたは2つで始まる名前は すべて何らかの使用のために予約されているので、処理系が標準ヘッダ等で使う ことができます(逆にユーザは '_' 1つまたは2つで始まる名前は定義しては いけない)。 これは少々窮屈な規定です。規格外の伝統的な名前はすべて '_' で始まるも のに変更しないと Standard C では使えないということになります。Standard C のライブラリや標準ヘッダの規定の出発点となった POSIX でも、Standard C の 規格外の名前は #ifdef _POSIX_SOURCE ... #endif で囲むことにしていますが、少なくともこの部分を使う時は処理系は Standard C ではなくなってしまいます。 しかも、例えば open(), creat(), close(), read(), write(), lseek() 等の 関数名が標準ヘッダには現れていなくても、fopen(), fclose(), fread(), fgets(), fgetc(), fwrite(), fputs(), fputc(), fseek(), etc. の関数が open(), etc. を使って実装されていれば、間接的にユーザの名前空間を侵害す ることになります。したがって、表向きだけ open(), etc. を _POSIX_SOURCE で分けたり 等の別ヘッダにしたりしても、意味がありません。 こうした「システムコール関数」は '_' で始まる名前に変更されるか、それ とも事実上必須のものは Standard C に取り入れられるか、どちらかを期待する しかありません。 2.は規約に明記されていることではありませんが、標準ヘッダが他の標準ヘ ッダを include すると、通常はその標準ヘッダが宣言できない名前が宣言され る結果になるので、1.にひっかかります。 を各標準ヘッダが include したりするのは反則です。これを避けるためには とい った別の名前の標準ではないヘッダを用意して、標準ヘッダが( 自 身も)これを include するようにすれば良いのです。そして、そこで使われる 名前はすべて '_' で始まるものにすることです。*4, *5 3.は実際に問題となることはないでしょう。 4.は古い処理系では問題のあったところですが、現在の処理系ではほとんど 対応できているようです。 #ifndef _STDIO_H #define _STDIO_H ... #endif といった皮で標準ヘッダの全体をくるむ方法が一般的です。そのほか、#pragma once といった拡張 directive を使う方法もあります。 5.で問題となるのは、sizeof やキャストを使ったマクロが標準ヘッダに書 かれている処理系があることです。Standard C では、#if 式に sizeof やキャ ストは使えません。実際に標準ヘッダのマクロで sizeof やキャストを使ってい る処理系では、#if でも sizeof やキャストが使えるようですから(Borland C)、 これは拡張仕様のつもりなのでしょう。 ユーザが自分のプログラムで #if 行に sizeof やキャストを使わない限り、 portability の問題が発生することはなく、他の問題も発生することは実際には まずないでしょうが、しかしこのプリプロセスの実装は Standard C の「拡張」 ではなく「逸脱」だと言わざるをえません。なぜなら、Standard C では #if を 処理するのは translation phase 4 であり、この phase では keyword は存在 しないのです。Keyword は phase 7 で初めて認識されます。Phase 4 では keyword と同じ名前はすべて単なる identifier として扱われ、#if 行ではマク ロとして定義されていない identifier はすべて 0 と評価されるので、sizeof (int) は 0 (0) と、(int)0x8000 は (0)0x8000 となり、violation of syntax rule となります。これに対して処理系は診断メッセージを出さなければなりま せん。診断メッセージを出さないのは、Standard C の「拡張」ではなく「逸脱」 です。また、そもそも phase 4 で一部の keyword だけ認識するというのは、プ リプロセスの論理構成として無理があり、compile phase (translation phase 7) の "pre"process phase としての意味を混乱させるものだとも言えます。*6 百歩譲ってこれを「拡張仕様」として認めるとしても、少なくとも sizeof や キャストを含む #if 行に対してはウォーニングを出すべきでしょう。 *1 ANSI C 4.1.2.1 (C90 7.1.3) Reserved identifiers 予約済み識別子 C99 7.1.3 Reserved identifiers *2 ANSI C 4.1.2 (C90 7.1.2) Standard headers 標準ヘッダ C99 7.1.2 Standard headers *3 ANSI C 4.1.6 (C90 7.1.7) Use of library functions ライブラリ関数の 使用法 C99 7.1.4 Use of library functions *4 次の本ではこの方法が使われている。この本はことに処理系の実装にとっ て、参考となる点が多い。 P. J. Plauger "The Standard C Library", 1992, Prentice Hall *5 GNU の glibc のシステムでは、標準ヘッダ自身によって 等 の他の標準ヘッダが複数回読み込まれるようになっているが、その時に定義 されるのは予約された範囲の名前だけのようである。これは規格違反とは言 えない。しかし、標準ヘッダの readability を損ない、メンテナンスを困 難にするので、良い方法ではない。 といったファイルを使う ほうが良い。 *6 [1.3], [2.4.14] 参照。 [4.1.2] 次に個々の標準ヘッダを見ていきます。いくつかの処理系に付属する標準ヘッ ダを見ると、最も問題の多いのは のようです。この 2つは最も簡単なヘッダなのですが、Standard C で新しく規定されたものであ るためか、実装を間違えることがあるようです。この2つについては、使い方に も少し触れます。 まず です。*1, *2 他の標準ヘッダと違ってこれだけは、何回 include しても同じというわけで はありません。NDEBUG というマクロをユーザが定義するかしないかによって、 include するたびに結果が変わります。すなわち、このヘッダは必要に応じてあ ちこちで #undef NDEBUG #include assert( ...); という使い方をするのです。そして、デバッグのすんだところから #define NDEBUG #include assert( ...); としていきます。NDEBUG が定義されていれば、assert( ...); がソースに残っ ていても、マクロが展開されると消えてしまいます(... が副作用を持つ式であ っても、式の評価がされないので、副作用は発生しないことに注意)。 こういう使い方ができるためには、 は #ifndef _ASSERT_H #define _ASSERT_H ... #endif などという皮でくるんではいけません。#pragma once なども、もちろん書いて はいけません。 また、このことからわかるように、assert() はマクロであり、NDEBUG によっ てその定義が変わります。 はいちいち #undef assert とした上で、 NDEBUG に応じて assert マクロを定義しなおす必要があります。 assert( expression) という呼び出しでは、NDEBUG が定義されていなければ、 expression が真であれば何もせず、偽であればその旨を標準エラー出力に報告 します。この報告は、expression をそのまま字句通りに表示し(マクロがあっ ても展開せず)、そのソースのファイル名と行番号を表示します。これはプリプ ロセッサに # 演算子と __FILE__, __LINE__ マクロが正しく実装されていれば、 簡単に実現できます。 実際には少し古い処理系では、# 演算子が正しく実装されていなかったり、 の理解を間違えているものが見られます。この Validation Suite のサンプルには を include しているものが多くありますが、 が正しく書かれていないとプリプロセッサ自身のテストが正確にで きません。正しい は簡単に書けるので、処理系付属のものがおかし ければ書き直しておいたほうが良いでしょう。次のものは C89 Rationale 4.2.1. 1 に例示されているものです。もちろんこれでも、# 演算子が正しく実装されて いなければ正確な結果にはなりませんが、それはプリプロセッサの問題なので、 やむをえません。 #undef assert #ifndef NDEBUG # define assert( ignore) ((void) 0) #else extern void __gripe( char *_Expr, char *_File, int _Line); # define assert( expr) \ ((expr) ? (void)0 : __gripe( #expr, __FILE__, __LINE__)) #endif __gripe() という関数は次のように書けます(もちろん、__gripe という名前 は '_' で始まるものなら何でもよい)。 #include #include void __gripe( char *_Expr, char *_File, int _Line) { fprintf( stderr, "Assertion failed: %s, file %s, line %d\n", _Expr, _File, _Line); abort(); } __gripe() 等の関数を使わず fprintf() あるいは fputs() や abort() 等を 直接 に書いている処理系も見られます。それもやってできないこと ではありませんが、そのためにはこれらの関数の宣言が必要です。FILE や stderr の宣言も必要です。しかし、 を include するわけにはいかな いので、かなりやっかいです。別関数を実装するほうが間違いがありません。 また、ささいなことですが、すべてをマクロで実現した場合は、呼び出しのた びに文字列リテラルが重複して生成されます。重複した文字列リテラルを1つに マージする最適化を処理系がしない場合は、コードサイズの上でも得策ではあり ません。 *1 ANSI C 4.2 (C90 7.2) Diagnostics 診断機能 C99 7.2 Diagnostics *2 C99 では、assert() マクロがどの関数から呼び出されたかも表示するこ とになった。こうした用途のために、__func__ という内部識別子が規定さ れている。 [4.1.3] この標準ヘッダは整数型の範囲等を表すマクロを書いておくものです。これら のマクロの書き方は、その値が規定と合致していることはもちろんですが、さら に次の条件を満たしている必要があります。*1 1.#if directive で使える整数定数式であること。 2.対応する型のオブジェクトが integral promotion を受けた結果の型と同 じ型の式であること。 キャストを使っている処理系がありますが、#if directive での sizeof やキ ャストが Standard C の範囲にないことは、[4.1] で論じました。そもそも、 が新しく規定された意味は、キャストや sizeof のような実行時環 境に関する照会を、プリプロセッサがする必要がないようにすることにあります。 例えば、 #if (int)0x8000 < 0 とか、 #if sizeof (int) < 4 とかとする代わりに、 #include #define VALMAX ... #if INT_MAX < VALMAX とするのです。#if では のマクロを使えば、キャストや sizeof を 使う必要はないはずです。 のマクロが型を間違えている例は、時々見られます。これはその プリプロセッサの仕様からくるものではなく、 を書く人が上記の2. および integral promotion(汎整数拡張)と usual arithmetic conversion (通常の算術型変換)の規則、さらに整数定数トークンの評価の規則のどれかを 失念することから起こるもののようです。 例えば、次のような定義をしているものがあります。 #define UCHAR_MAX 255U unsigned char の値は(CHAR_BIT が 8 であれば)すべて int の範囲におさ まるので、unsigned char 型のデータオブジェクトの値は integral promotion によって int になります。したがって UCHAR_MAX も int として評価されるも のでなければなりません。ところが、255U では unsigned int になってしまい ます。これは #define UCHAR_MAX 255 としなければならないのです。 どちらでも実用上は問題ないのではないかと思うかもしれませんが、必ずしも そうではありません。符号無しの型を含む演算は「通常の算術変換」を引き起こ し、同じサイズの符号付きの型の符号無しへの変換を強制します。そのため、大 小比較の結果が違ってくるのです。 assert( -255 < UCHAR_MAX); としてみると、わかるでしょう。 この間違いには、integral promotion と usual arithmetic conversion の規 則が Standard C ではそれまでの多くの処理系で採用されていたものから変更さ れた、という事情が関係しています。K&R 1st. には unsigned char, unsigned short, unsigned long という型はありませんでしたが、その後、多くの処理系 に実装されるようになり、そしてそれらの処理系の多くでは、符号無しは常に符 号無しに変換されることになっていました。 しかし、Standard C の integral promotion では、unsigned char, unsigned short はそれぞれすべての値が int の範囲におさまる限りは int に promote され、そうでない場合だけ unsigned int に promote されます。また、 unsigned int と long との間の usual arithmetic conversion では、unsigned int のすべての値が long の範囲におさまるのであれば long に、そうでない場 合だけ unsigned long に変換されます。"unsigned preserving rules" から "value preserving rules" への変更と呼ばれるものです。このほうが直観的に 意外性が少ないというのが、この規定の理由だとされています。 で は、この規則に注意が必要です。*2 以下の例ではすべて、short は16ビット、long は32ビットとします。 USHRT_MAX の値は 65535 ですが、その書き方は int が16ビットの場合と32 ビットの場合とで違ってきます。 #define USHRT_MAX 65535U /* int が16ビットの場合 */ #define USHRT_MAX 65535 /* int が32ビットの場合 */ unsigned short は int が16ビットの場合は int の範囲におさまらないの で、unsigned int に promote されます。したがって、USHRT_MAX も unsigned int として評価されるものでなければなりません。65535 では long として評価 されてしまいます。接尾子 'U' が必要なのです。他方で int が32ビットの場 合は unsigned short の値はすべて int におさまるので、int に promote され ます。したがって USHRT_MAX も int として評価されるものであることが必要で す。'U' を付けてはいけません。ところが、これが逆になっている例が見られま す。 #define USHRT_MAX 0xFFFF これなら、int が16ビットでも32ビットでも正しく評価されます。 Standard C では、U, u, L, l という接尾子がどれも付かない8進または16進 の整数定数トークンは、int, unsigned int, long, unsigned long の順で、非 負でその値を表現できる型に評価されます。すなわち、0xFFFF は int が16ビ ットなら unsigned int の 65535、int が32ビットなら int の 65535 と評価 されるのです。これに対して、接尾子の付かない10進整数定数トークンは、 int, long, unsigned long の順で評価されます。65535 は int が16ビットな ら long、int が32ビットなら int で評価されることになります。*3 C99 では、 long long / unsigned long long が追加されました。0 か 1 の 値しか持たない _Bool という型も追加されました。他の型の整数もオプション で実装できるようになりました。Integer promotion のルールも拡大され、 unsigned long で表現できない整数定数トークンは long long / unsigned long long で評価されることになります。 また、整数型が増え、処理系定義の整数型も認められたのに伴って、型の大小 関係がわかりにくくなったため、integer conversion rank(整数変換ランク) という概念が導入されました。この概念はやや複雑ですが、実用上は気にする必 要はありません。標準の整数型ではランクの大小関係は次の通りです。 long long > long > int > short > char > _Bool ここで、たとえば long と int がともに 32 ビットというようなサイズの同 じ実装であっても、ランクの大小は区別されるというところがポイントです。 * 4, *5 *1 ANSI C 2.2.4.2.1 (C90 5.2.4.2.1) Sizes of integral types 各整数型の大きさ C99 5.2.4.2.1 Sizes of integer types *2 ANSI C 3.2.1 (C90 6.2.1) Arithmetic operands 算術オペランド C99 6.3.1 Arithmetic operands *3 ANSI C 3.1.3.2 (C90 6.1.3.2) Integer constants 整数定数 C99 6.4.4.1 Integer constants *4 C99 6.4.4.1 Integer constants *5 C99 では integer types の処理系による相違を吸収するための および という標準ヘッダが追加されている。64 bits シ ステムの登場によって integer types の種類が増えて、対応関係がわかり にくくなってきたため、long とか short とかの名前のほかにいくつかの型 名を typedef しようというものである。しかし、この型名が 26 種もあり、 それに対応する最大値・最小値を表すマクロが 42 種、対応する fprintf() の format specifier に変換されるマクロが 56 種、同様に fscanf() の format specifier に変換されるマクロが 56 種にも上る。処理系の負担は あまりないとは言え、煩雑すぎて、いささか末期症状のような印象もないで はない。 [4.1.3.1] INT_MIN のマクロのうちで最も混乱の見られるのが、2の補数の内部表現 を持つシステムでの INT_MIN と LONG_MIN です。中でも int が16ビットで long が32ビットの処理系での INT_MIN が、上記の問題をすべて見せてくれま す。そこで、特にこれを項を分けて取り上げてみます。 この場合、int の範囲は [-32768,32767] であることは言うまでもありません。 そして、INT_MAX についてはどの処理系も 32767 あるいは 0x7FFF としていて、 問題ありません。ところが、INT_MIN が次のように定義されている例を見掛けま す。 #define INT_MIN (-32767) なぜこのような実際と違う定義をしているのでしょうか? 他方で、さすがにこう定義している処理系は見当たりません。 #define INT_MIN (-32768) -32768 は -, 32768 の2つのトークンから成っています。そして、32768 は int で表現できる範囲にありません。そこで、これは long で評価されます。し たがって、-32768 は - (long) 32768 の意味になってしまうのです。 中には、こう定義しているものもあります。 #define INT_MIN ((int)0x8000) キャストを使った定義については、コメントは繰り返しません。0x8000 だけ では (unsigned) 32768 の意味になるので、やはり不可です。 では、どう定義すれば、キャストを使わずに (int) -32768 と評価されるので しょうか? #define INT_MIN (-32767-1) これで良いのです。32767 は INT_MAX でも 0x7FFF でもかまいません。この 定義には引き算という演算が含まれていますが、それは問題ありません(そもそ も単項の - も演算子である)。*1, *2 #define INT_MIN (~INT_MAX) #define INT_MIN (1<<15) これらも正しい定義です。 #define INT_MIN (-32767) これは演算をするという着想が浮かばなかったために、正しい値に定義するこ とをあきらめたものと推測されます。 では、-32767 という定義は間違いでしょうか? それとも、これも正しい定 義なのでしょうか? 結論を言えば、これは間違いだと考えられます。 INT_MIN は int の最小値を表すマクロと規定されています。もし INT_MIN が -32767 だとすると、これはいったい何を意味するものなのでしょうか? そし て、INT_MIN-1 はいったい何なのでしょうか? あるいは ~INT_MAX や 1<<15 は何でしょうか? この場合の INT_MIN-1 については、浮動小数点演算の「非数」のような範囲 外を表すビットパターンとする考え方があるようです。 しかし、整数型に関する Standard C の規定と照らし合わせると、この解釈は 成り立つ余地がありません。まず、整数型についてのビット演算の結果は、op1 << op2 や op1 >> op2 の op2 の値が負数であるか op1 の型のビット数以上で ある場合が undefined ですが、それ以外には undefined な場合はなく、すべて 整数型の一意の値を返します。~op は op が int であれば、その結果も int で あり、op1 & op2, op1 | op2, op1 << op2, op1 >> op2 は op1, op2 がともに int であれば、その結果も int です。したがって、~INT_MAX も 1<<15 も結果 は int なのです。1<<15 は overflow すると思うかもしれませんが、そうでは ありません。ビット演算ではビット操作をした結果のビットパターンに対応する 値を返すので、overflow は発生しようがないのです。 Cでは一般に整数型の演算はよく定義されていて、undefined な部分はきわめ て少ないものです。ことにビットパターンと値との関係は、1の補数や符号+絶 対値の内部表現で 0 に2つのビットパターンがある以外は、完全に一対一に対 応しています。これは K&R 1st. から Standard C まで一貫したものです。また、 ビットパターンそのものを表記する方法はCにはなく、「非数」を表記しように も (-32767-1) 等と書くしかありませんが、これは見ての通りの int の値その ものです。C89 Rationale はいくつかの根拠を挙げて、整数型には「無効な整数」 とか「不正な整数」を表すビットパターンの存在する余地のないことを明らかに しています。*3, *4 こういうことで、2の補数の内部表現では ~INT_MAX が INT_MIN の値であり、 それより大きい INT_MIN の定義は間違いだと言わざるをえません。 *1 この定義を私が初めて見たのは、P. J. Plauger "The Standard C Library" である。最近の処理系にはこの流儀の limits.h が多くなってい るようである。 ただ、この本の limits.h にも間違いがあるのである。int が16ビットで long が32ビットの処理系のための定義が次のようになっている。 #define UINT_MAX 65535 #define USHRT_MAX 65535 これでは long で評価されてしまう。正しくはこうである。 #define UINT_MAX 65535U #define USHRT_MAX 65535U *2 最近の処理系では *_MIN は (-*_MAX - 1) という形の定義が一般的にな り、間違いは少なくなっている。しかし、まだ時に間違いが見られる。 Visual C++ .net 2003 の Vc7/include/limits.h, Vc7/crt/src/include/ limits.h には次のようなものがある。 #define LLONG_MIN 0x8000000000000000 0x8000000000000000 は unsigned long long で評価される。この型は最高 位ランクであるので、integer promotion の結果も同じ型である。マイナス 値には決してならない。したがって、 #if LLONG_MAX > LLONG_MIN は期待した結果にはならない。 LCC-Win32 V.3.2 の include/limits.h の LLONG_MIN はこうなっている。 #define LLONG_MIN -9223372036854775808LL 9223372036854775808LL はこのトークンの値がすでに signed long long の 範囲を overflow しており violation of constraint である。 #define LLONG_MIN -9223372036854775808LLU とすれば 9223372036854775808LLU は unsigned long long になるが、符合 無し型に unary - 演算を施しても、結果の型は変わらないので、unsigned long long で表現できない値となり、undefined である。 Visual C++ も LCC-Win32 も他の *_MIN の定義はすべて (-*_MAX - 1) と なっているのであるが、LLONG_MIN だけ間違っているのはどうしたことであ ろうか。 #define LLONG_MIN (-LLONG_MAX - 1LL) とすれば何も問題はない。 *3 C89 Rationale 3.1.2.5 Types C99 Rationale 6.2.6.2 Integer types *4 C99 では、特定のビットパターンを例外処理を引き起こす "trap representation" として扱うことも、処理系に許されることになっている。 実際にどういう処理系が該当するのかは、私は知らない。 [4.1.4] ISO C 9899:1990 / Amendment 1 では、iso646.h という標準ヘッダが追加さ れました。これは &, |, ~, ^, ! を含む演算子を ISO 646 の invariant character set だけで表す代替 spelling を提供するものです。|, ~, ^ は trigraphs でも代替 spelling が提供されていますが、trigraphs は readability に欠けるので、iso646.h はそれに代わって11種の演算子をトー クン単位でマクロで定義しています。 この実装はきわめて簡単で、次のようなもので十分です。プリプロセスでマク ロ展開されるので、処理系にとっても何も面倒はありません。* /* iso646.h ISO 9899:1990 / Amendment 1 */ #ifndef _ISO646_H #define _ISO646_H #define and && #define and_eq &= #define bitand & #define bitor | #define compl ~ #define not ! #define not_eq != #define or || #define or_eq |= #define xor ^ #define xor_eq ^= #endif * C++ Standard では、これらの identifier 様 operator はマクロではなく operater-token とされている。処理系にとってはやっかいな、無意味な仕 様である。 ☆ 5.各種プリプロセッサのテスト結果 ☆ [5.1] テストしたプリプロセッサ テストした処理系と実行方法は次の通りです。処理系はリリースされた時期の 順に並べてあります。 実行時オプションは C95 (C90), C99, C++98 のそれぞれで少しずつ異なりま す。 処理系付属の , に問題がある場合は、正しく書き直し てからテストしています。 番号 OS 処理系 実行プログラム(版数) 実行時オプション コメント 1 : Linux / / DECUS cpp C95: cpp Martin Minow による DECUS cpp のオリジナル版 (1985/01)。当時の DEC の各種システム、UNIX, MS-DOS のいくつかの処理系に移植されているが、 このテストで使ったのは、kmatsui がオリジナル版に手を加えて Linux / GNU C でコンパイルしたもの。Translation limits がなるべく規定をクリ アするようマクロを書き換えた。 2 : MS-DOW / Borland C++ V.4.02J / cpp C95: cpp -A -w -i250 bcc -A -w -i250 1993 年。日本語版は 1994/12 のもの。Trigraph は cpp も bcc も処理せ ず、その代わりに trigraph.exe という変換プログラムが用意されている。 「規格準拠」と称するためのアリバイ作りである。Borland C では、これを 使って予め変換したサンプルでテストした(甘いテスト)。ところが、この trigraph.exe は による行接続まで処理してしまう。 そのため、行番号がズレる(q.1.2 のほうで減点)。 3 : FreeBSD 2.2.7 / GNU C V.2.7.2.1 / cpp (V.2.0) GO32 / DJGPP V.1.12 / cpp (V.2.0) WIN32 / BC 4.0 / cpp (V.2.0) MS-DOS / BC 4.0, TC 2.0 / cpp (V.2.0) MS-DOS / LSI C-86 V.3.3 / cpp (V.2.0) OS-9/6x09 / Microware C/09 / cpp (V.2.0) C95: cpp -23 (-S1 -S199409L) -W15 gcc -ansi -Wp,-2,-W15 C99: cpp -23 (-S1) -S199901L -W15 C++: cpp -23+ -S199711L -W15 kmatsui による free software (1998/08)。MCPP と呼ぶ。DECUS cpp をベ ースとして書き直したもの。 OS-9/09 版や MS-DOS 版では translation limits の一部がクリアできない。 MCPP をコンパイルする処理系が long long を実装していない場合は、#if 式が long でしか評価できない。 下記の採点は、long long を持つ 32 bits システムの処理系でコンパイル した版を使った場合のものである。当時はこのバージョンは上記の処理系に 対応していたが、今回は Linux 上で GNU C V.3.3 でリコンパイルしたもの を使ってテストした。 4 : WIN32 / Borland C++ V.5.5J / cpp32 (2000/08) C95: cpp32 -A -w bcc32 -A -w C99: cpp32 -A -w C++: cpp32 -A -w Trigraph については V.4.0 と同様。Free の処理系となったが、そのかわ りドキュメントは help だけとなった。 5 : Vine Linux 3.1, CygWIN 1.3.10 / GNU C V.2.95.3 (2001/03) / cpp0 C95: cpp0 -D__STRICT_ANSI__ -std=iso9899:199409 -$ -pedantic -Wall gcc -ansi -pedantic -Wall C99: cpp0 -std=c9x -$ -Wall C++: g++ -E -trigraphs -$ -Wall GNU C は portable なソースであるので、特定のシステムに移植したものの 仕様は移植者が用意すべきであるが、そうした独自のドキュメントは提供さ れていない。cpp のドキュメントとしてはやはり GNU の cpp.info しか存 在しない。 6 : Vine Linux 3.1 / GNU C V.3.2R (2002/08) / cpp0 C95: cpp0 -D__STRICT_ANSI__ -std=iso9899:199409 -$ -pedantic -Wall gcc -std=iso9898:199409 -pedantic -Wall C99: cpp0 -std=c99 -$ -Wall C++: g++ -E -trigraphs -$ -Wall ソースから kmatsui がコンパイルしたもの。--enable-c-mbchar というオ プションを付けて configure している。 7 : Vine Linux 3.1 / / ucpp (V.1.3) C95: ucpp -wa C99: ucpp -wa Thomas Pornin による free software (2003/01)。Portable な単体プリプ ロセッサ。Linux / GNU C でコンパイルしてテストした。 8 : WIN32 / Visual C++ .net 2003 / cl C95: cl -Za -E -Wall -Tc C99: cl -E -Wall -Tc C++: cl -E -Wall -Tp -E オプションではコメントや が正しく処理されな いので、コンパイルによるテストを併用 (2003/04)。 9 : WIN32 / LCC-Win32 V.3.2 / lcc C95: lcc -A -E lcc -A C99: lcc -A -E C++: lcc -A -E Jacob Navia が C. W. Fraser & Dave Hanson の free software である lcc を元に書いた統合開発環境 (2003/08)。ソースは shareware。プリプロ セス部分のソースは Dennis Ritchie が Plan9 のために書いたもの。 10 : WIN32 / / wave (V.1.0.0) C95: wave C99: wave --c99 C++: wave Hartmut Kaiser による free software。Stand-alone のプリプロセッサ。 Boost preprocessor library なるものを使って実装されている。WIN32 用 の実行プログラム (2004/01) でテストした。 11 : VineLinux 3.1 / GNU C V.3.4.3 (2004/11) / cc1, cc1plus C95: gcc -E -std=iso9898:199409 -pedantic -Wall C99: gcc -E -std=c99 -$ -Wall C++: g++ -E -std=c++98 -$ -Wall ソースを筆者が GNU C 3.3.2 でコンパイルしたもの。 12 : FreeBSD, Linux, CygWIN / GNU C / mcpp_std (V.2.5) : WIN32, MS-DOS / BCC, LCC-Win32, ... / mcpp32_std (V.2.5) C95: mcpp_std -23 (-S1 -V199409L) -W31 gcc -ansi -Wp,-2,-W31 C99: mcpp_std -23 (-S1) -V199901L -W31 C++: mcpp_std -23+ -V199711L -W31 MCPP V.2.5 (2005/03) の Standard モード。 [5.2] 採点表 [5.3] 各プリプロセッサの特徴 1 : Linux / / DECUS cpp ANSI draft 初期のころのもので、今となっては規格準拠度は低くなっていま す。診断メッセージはまずまずですが、ドキュメントはほとんどありません。し かし、骨格のしっかりしたプログラムで、安定しています。 ソースは portability の高いもので、いくつかの処理系に移植されていまし た。お手本のように読みやすいソースで、読むだけでも勉強になります。私が MCPP のベースにしたのはこのソースです。 2 : MS-DOS / Borland C++ V.4.02J / cpp 4 : WIN32 / Borland C++ V.5.5J / cpp32 C90 の規格準拠度は比較的高く、やっかいな shift-JIS にもまともに対応し ています。ドキュメントも整っています。しかし、e_* では診断メッセージはた いてい出ますが、間に合わせ的なものが多く、質は良くありません。 「規格外の品質」は貧弱で、これという拡張機能もありません。Undefined な 部分に対しては診断メッセージはあまり出ず、時々暴走します。規格に対応させ るだけで精一杯となってしまったようです。 速度は Turbo C と違って、速いほうではなくなりました。すでに1パス・コ ンパイラのメリットはなくなってデメリットだけが残っていると思われますが、 いつまでこの流儀を続けてゆくのでしょうか。 BC 5.5 のプリプロセッサは BC 4.0 に比べて、ほんの少ししか改善されてい ません。Free になったことによって、逆にドキュメントが不足する状態となっ ています。 5 : Linux, CygWIN / GNU C V.2.95.3 / cpp0 C90, C95 の規格準拠度はかなり高く、診断メッセージも的確です。動作はほ ぼ安定しており、かつきわめて高速です。豊富すぎるくらい豊富なオプションも 特徴です。MCPP でも、それらのオプションのいくつかをまねています。 かつてはいくつかの痛いバグもありましたが、V.2.95 ではバグはほとんどな くなっています。 残る問題は、C99, C++98 の新仕様の多くが未実装であること、診断メッセー ジがまだまだ足りないこと、ドキュメントがかなり不足していること、#pragma を使わない規格違反の拡張機能が多いこと、pre-Standard の obsolete な仕様 が多く隠れていること、multi-byte character encoding への対応が中途半端で 実用レベルに達していないこと、等です。 cpp.info というドキュメントは GNU C / cpp および Standard C プリプロセ ス全体の解説として優れたものです。ただ、処理系定義部分のドキュメントが CygWIN, FreeBSD, Linux のどれにも存在しないのは残念です。「移植」という のはプログラムだけではないはずですが。 ソースはつぎはぎだらけで読みにくく、かつプログラムの組み立てが古いマク ロプロセッサの構造を引きずっています。しかし、 GNU C 処理系の全体が優れ ているため、多くのシステムに移植されています。 6 : Linux / GNU C V.3.2 / cpp0 GNU C V.3 ではプリプロセスは V.2 からソースが一新されました。同時にド キュメントも一新されました。Token-based な原則が徹底され、pre-Standard な仕様はまだ許容しながらもウォーニングが出るようになり、undocumented な 仕様が減少しています。全体としては私の期待した方向へ大幅に改良されていま す。プログラム構造が一新されたことで、今後の改良もしやすくなったものと思 われます。 診断メッセージ・ドキュメント・C99 対応・multi-byte character への対応 はまだ不十分ですが、今後、改良されてゆくことでしょう。速度はやや遅くなり ましたが、それでもまだ速いほうです。 ただ、ヘッダファイルが複雑化して include directory のサーチ順の設定も 複雑になってきているのは、やっかいな問題です。また、古いオプションが使わ れなくなってきた一方で多くの新しいオプションが増えており、なかなか整理す る方向へ進みません。V.3 ではプリプロセスがコンパイラ本体に一体化されまし たが、プリプロセス部分とコンパイル部分との内部的な interface がなぜか複 雑なものになっているのも、残念なことです。 7 : Linux / / ucpp (V.1.3) C99 に対応していること、オープンソースで portable であることが特徴です。 規格準拠度はかなり高くなっています。UCN と UTF-8 に対応していることにな っていますが、これはきわめて不十分です。診断メッセージはやや漠然としてい ます。ドキュメントもかなり不足しています。 8 : WIN32 / Visual C++ .net 2003 / cl C99 の仕様はまだほとんど実装されていません。C90 以前からの仕様にもいく つかのバグが残っています。Translation phase の混乱していることが最も根本 的な問題です。非常に古いソースに部分的に手を入れながらバージョンアップを 繰り返してきていることが想像されます。 診断メッセージはややピントのずれたものがしばしば見られます。エラーが起 こると処理を中断してしまうことが多いのも、使いにくい点です。ドキュメント は実装に比べて更新がかなり遅れているようです。 Translation limits が大きいことと #pragma が比較的多いことがとりえと言 えます。ことに #pragma setlocale は便利なものです。しかし、C90 でも # pragma 行がマクロ展開されること、にもかかわらず #pragma sub-directive が ユーザの名前空間を使っているのは問題です。 添付されているシステムヘッダには、例外はありますが全体としては問題は少 ないようです。 9 : WIN32 / LCC-Win32 V.3.2 / lcc プリプロセスの部分は Dennis Ritchie の Plan9 用のソースに Jacob Navia が手を加えたものですが、デバッグが不足しており、#if 式の評価などにかなり のバグがあります。C95 以降の仕様には対応していません。ドキュメントも不足 しています。 10 : WIN32 / / wave (V.1.0.0) C++ の STL のための "meta-programming" に使うことを第一の目的に作られ たもので、そのための拡張機能をいくつか持っています。実装の大半が C++ の ライブラリでできていて、ソースの大半がヘッダファイルで成り立っているとい う特異なプリプロセッサです。"Meta-programming" の実例では再帰的マクロが 多用されており、その展開では MCPP の -c オプションや GNU C / cpp と同様 の、「再置換禁止ルール」の適用範囲を狭く限定する方法をとっています([2.4. 26] 参照)。 一般の C/C++ プリプロセッサとしても使えることを意図しており、C++98, C99 対応を標榜していますが、完成度はまだかなり低いものです。しかし、V.1. 0.0 のあとで、MCPP 2.4 に付属する検証セットでテストして多くのバグを修正 したとのことです。この検証セットを使っていれば、良いプリプロセッサになっ てゆくことでしょう。 一方で、MCPP の再帰的マクロの展開ルーチンは GNU C / cpp の testsuite と wave の testcases を使って修正されました。ただし、wave の testcases には wave 専用のものが多く含まれており、またいくつかの testcases には規 格の解釈の間違いが見られます。 11 : Linux / GNU C V.3.4.3 / cc1, cc1plus スコアは V.3.2 とほとんど変わりません。しかし、プリプロセスの組み立て はかなり変わりました。V.3.2 ではスッキリした方向性を示していた GNU C で すが、V.3.3, 3.4 では方向が変わったようです。単体のプリプロセッサを廃止 し、大量のマクロを事前定義し、V.3.2 で obsolete とされた仕様の一部を復活 させ、巨大な1つのコンパイラにすべてを吸収する方向に進んでいるのは、はた して改善と言えるかどうか疑問のあるところです。Multi-byte character の encoding で UTF-8 に特権的な位置を与えたことも、multi-lingualization の 多様な可能性を狭めることになるのではないでしょうか。 3 : FreeBSD, WIN32, MS-DOS / / MCPP (V.2.0) 12 : FreeBSD, Linux, CygWIN, WIN32, MS-DOS / / MCPP (V.2.5) 私が自分で作って自分でテストしているものなので、規格準拠度は当然、最高 です。世界一正確なプリプロセッサのはずです。診断メッセージの豊富さと的確 さ、ドキュメントの詳細さも随一です。有用なオプションと #pragma も一通り 持っています。C99 の仕様にもすべて対応しています。 ソースの移植性の良さも一番でしょう。移植の実績が限られていることだけが 問題です。皆さん、よろしくお願いします。 [5.4] 総評 こうして多くのプリプロセッサをテストしてみると、最近では C90 規格準拠 度はかなり高いものが多くなってきていることがわかります。しかし、各処理系 ともまだまだ多くの問題を持っています。なお、MCPP はほとんどの項目が満点 なので、一々言及しません。 n_* のサンプルに関しては正しく処理するものが増えており、GNU C 2.95,3.2, 3.4, BC (Borland C) 4.0,5.5 , LCC-Win32 3.2, ucpp 1.3, Visual C++ .net 2003 は実用上はあまり問題のないレベルに達しています。しかし、どの処理系 にも意外なバグが見られます。 最も驚いたのは、n_13_7.t (n_13_7.c) が division by 0 のエラーになって しまう処理系が Visual C を含めてしばしば見られることです。&&, || や3項 演算子での「評価の打ち切り」というCの基本的な仕様を満たしていないのです。 BC 4.0,5.5 は n_13_7.c ではウォーニングにとどめていますが、e_14_9.c の本 当の division by 0 でも同じウォーニングしか出ません。Turbo C では本当の 0 除算でも評価をスキップされる部分式でもどちらも同じエラーとなっていまし たが、Borland C はその診断メッセージをウォーニングに格下げしただけなので す。この処理系の「間に合わせ診断メッセージ」の一例です。 Standard C で新しく規定された仕様では、# 演算子による文字列化の実装の 間違っているものが時々あります。引数中に文字列リテラルや文字定数があって、 その中に escape sequence のあるものは、いまだに移植性が保証される状況に なっていません。これは特殊な例のように思えるかもしれませんが、assert() マクロ等を使うと「二重の文字列化」がしばしば発生するので、さほど特殊なこ とではありません。 そのほか、各処理系ごとにバグの一つや二つは必ず見出されます。 Amendment 1, Corrigendum 1 で追加された仕様は、ある程度実装しているの は GNU C 2.95,3.2,3.4, ucpp, Visual C .net です。 C99 の仕様は、ucpp だけがほぼ実装しています。// コメントは多くの処理系 が以前から実装しています。そのほか GNU C / cpp は long long を持ち、 translation limits にかなりの余裕があり、マクロのカラ引数も適切に処理し ます。可変引数マクロは GNU C 独自の仕様のものを持っていますが、2.95 では C99 の仕様のものも実装されました。3.2 では _Pragma() も実装されました。 UCN は ucpp だけが実装しています。GNU C 3.2,3.4 は文字列リテラルおよび文 字定数の中の UCN だけを実装しています。Wave は C99 の仕様の半分を実装し ています。 C++98 では UCN 以外の仕様を GNU C 3.2,3.4, wave が実装しており、Visual C .net も半分だけ実装しています。 C++98 の extended-character を UCN に変換するという奇怪な仕様を実装し ているものは、まだ見当たりません。 i_* の処理系定義部分の処理では、#if 式で wide character を扱えないもの が多いようです。しかし、wide character に限らず #if 式で文字定数を使うこ とは、規格にはあるもののほとんど意味のないことなので、使えないからといっ て困ることはないでしょう。こういう無意味なものは規格の仕様から除外すべき です。 Multi-byte character については、Visual C が多くの国の encoding に不十 分ながら対応していますが、他の処理系は貧弱です。GNU C 2.9, 3.2 は実装が 中途半端で、実用レベルに達していません。GNU C 3.4 ではすべての encoding を UTF-8 に変換してから処理するようになり、それによって多くの encoding に対応できるようになりましたが、実装はまだ実用レベルではありません。 Shift-JIS や BIG-5 を使うシステムでは、リテラルの tokenization や # 演 算子による文字列化に注意が必要です。Visual C はこれにうまく対応していま す。Borland C 4.0J,5.5J も shif-JIS には対応しています。 e_* に対する診断メッセージでは、GNU C 2.95,3.2,3.4 が最も優れています。 Visual C, ucpp では診断メッセージは比較的出るものの、漠然としていたりピ ントが外れていたりします。#if 式の overflow に対する診断メッセージの出る ものは少なく、BC, ucpp がある程度診断するだけです。 処理系定義部分のドキュメントでは、BC 4.0, GNU C 3.2,3.4 がまずまずのレ ベルで、他はどれもお粗末です。 u_* に対する診断メッセージでは、GNU C 3.2,3.4 がまずまずですが、他はお 粗末です。Undefined だから処理系は何もしなくて良いというものではないと思 いますが。 s_*, w_* についてはどの処理系もほとんど対処していません。コメントのネ ストに対するウォーニングさえ出すものが少ないのは、意外です。 「その他の各種品質」では、GNU C がその豊富なオプション、診断メッセージ の的確さ、高速性、移植性とで抜きん出ています。 総合すると、GNU C / cpp、ことに V.3 のものが、規格準拠度の高さ、使い勝 手の良さ、あまり大きな穴のない安定性等で、最も優れています。 もちろん、MCPP は速度だけは劣るものの、他のほとんどの面でこれを上回っ ていることは言うまでもありません。 さて、こうして大量のテストをやってきましたが、改めて思うのは、テスト用 サンプルというものがいかに重要かということです。MCPP はこのサンプルの作 成と並行してデバッグが進められてきたものです。十分なサンプルがなければバ グの存在にさえもなかなか気付かれませんから、デバッグどころではありません。 規格にもこうした網羅的なテスト用サンプルが添付されるようになると、各処 理系の品質は飛躍的に向上することでしょう。また逆に、網羅的なテスト用サン プルを作ることは、規格の問題点を浮き彫りにすることにもなります。テスト用 サンプルは規格のイラストレーションなのです。 [5.5] テスト報告とご意見を この Validation Suite についてのご意見や、これを使った各種処理系のプリ プロセスのテスト報告をお待ちしています。comp.std.c, fj.comp.lang.c 等の newsgroup またはメールでお願いします。 プリプロセッサの詳細テストを行った場合は、[5.2] 採点表を切り取って、こ れに書き込んでお送りください。合計点の計算には tool/total.c をコンパイル して使います。採点表を cpp_test.old とすると、 total 13 cpp_test.old cpp_test.new とすると、stotal (sub-total), mtotal (mid-total), gtotal (grand-total) の各欄が書き込まれて cpp_test.new に出力されます。"13" のところは処理系 の数を指定します。 GNU C の場合は、検証セットの testsuite 版による自動テストができます。 各種バージョンの GNU C のテスト結果をお送りください。ログファイル (gcc. sum, gcc.log) を送っていただくと、診断メッセージのバージョンによる違いを 検証セットの testsuite 版に採り入れることができます。 また、Validation Suite や MCPP の開発は CVS repository で進められてい ます。開発に参加してみようという方は、ぜひメールをください。 [eof]