フルスペックのprintf?

ときどきの雑記帖 i戦士篇で引用されていた2ちゃんねるのやり取りを見て、思わず画面に向かって突っ込んでしまったので、そのことを記録しておきます。printfを自前でフルスクラッチで(stdarg.h以外には依存せずに)書きたいということのようです。

結論からいえば不可能です。これは誰にでも簡単にわかるはずです。stdarg.hにしか依存できないのであれば、どうやって標準出力に書きこむのでしょうか? 少なくとも、fwrite、putc、fputc、putchar、fputs、fprintf、vprintf、vfprintfのどれかに依存しなければ標準出力への書き込みができません。趣旨を考えると、fwrite、putc、fputc、putcharのどれかを出力に使うべきかと思います。putchar以外を使う場合は、stdoutマクロも必要になります。

では、stdarg.hに加えてstdio.hにも依存してよいならどうでしょうか? これも結論をいえば不可能です。もっともシンプルな、C90のprintfでさえ不可能です。なぜなら、浮動小数点数を書式化する際の小数点文字がロケールに依存するからで、これはlocale.hで宣言されるlocaleconv関数を呼び出さなければ取得することができません。

C95、すなわちAMD1にも対応する場合はさらに状況が悪化します。C95では、書式指定として、%lcや%lsが使えるようになります。つまり、ワイド文字やワイド文字列を実引数に指定できるようになるのです。これを実現するには、少なくともワイド文字から多バイト文字に変換するための関数が必要になります。

ワイド文字から多バイト文字に変換するための標準関数といえば、wctombが真っ先に思い浮かびます。しかし、wctombでは役不足です。なぜなら、シフトシーケンスに依存するエンコーディングを使用している場合、printf呼び出し前の状態を破壊してしまうからです。ですので、wchar.hで宣言されるwcrtombが必要になります。

C99になると、もうひとつ状況が悪化します。C99では、書式指定として、%zuとか%tdとか%jdが使えるようになります。%zuのようにzを指定した場合は実引数がsize_t型であることを意味します。%tdのようにtを指定した場合は実引数がptrdiff_t型であることを意味します。size_t型はstdio.hで定義されますが、ptrdiff_t型はstddef.hでしか定義されません。つまり、どうしてもstddef.hに依存することになります。

%jdのようにjを指定した場合は、実引数がintmax_t型またはuintmax_t型であることを意味します。intmax_t型やuintmax_t型はstdint.hでしか定義されません。inttypes.hでも定義されますが、それは間接的にstdint.hをインクルードするからです。ということで、C99では依存するヘッダがさらに増えるわけです。

移植性を無視して特定処理系に特化するつもりであれば、stddef.hやstdint.hに依存する必要はないかもしれません。しかし、それをいうならstdarg.hに依存する必要もなくなります。printfは結構複雑なのです。

ついでにもう一言付け加えるなら、マルチスレッド対応にするには、putcなどではなく、putc_unlockedや_putc_nolockのような処理系依存の関数を使って出力を行い、printf(またはvfprintf)関数の最初と最後でflockfileなどで排他制御を行う必要があります。あるいは、出力する文字列をいったんメモリ上に組み立ててから、一気にfwriteなどで出力する必要があります。後者の場合は、当然reallocなどが必要になることでしょう。

この記事のトラックバックURL:

http://www.kijineko.co.jp/trackback/735
このエントリーを含むはてなブックマーク