syghの新フラグメント置き場

プログラミングTipsやコード断片の保管場所です。お絵描きもときどき載せます。

size_t, ptrdiff_tのprintf書式指定

(これは2010-12-05に書いた故OCNブログの記事を移植したものです)

size_tptrdiff_tはポインタ同様、32bit向け(_M_IX86とか)と64bit向け(_M_X64とか)でサイズと上下限が変わるプラットフォーム依存の整数型なので、printf()系の関数で出力する場合、書式指定(長さ修飾子、length modifier)に注意しないといけないのですが、MSVC(Visual C++)のCRTでは拡張書式として%Id, %Iuなどが用意されています。フォントによっては分かりづらいかもしれませんが、小文字のエルではなく大文字のアイです。

詳しくはMicrosoft Docs(旧MSDNライブラリ)のドキュメントを参照してください。

具体的な書式指定の仕方を挙げておきます。

printf("1L  << 16 by %%I32d = %I32d\n", 1L << 16);
printf("1LL << 48 by %%I64d = %I64d\n", 1LL << 48);
printf("INT32_MAX by %%I32d = %I32d\n", int32_t(INT32_MAX));
printf("INT64_MAX by %%I64d = %I64d\n", int64_t(INT64_MAX));
printf("-1 by %%I32u = %I32u\n", uint32_t(-1)); // 大抵の処理系で、UINT32_MAX と同義のはず。
printf("-1 by %%I64u = %I64u\n", uint64_t(-1)); // 大抵の処理系で、UINT64_MAX と同義のはず。
printf("PTRDIFF_MAX by %%Id = %Id\n", ptrdiff_t(PTRDIFF_MAX)); // ターゲット プラットフォーム依存。
printf("SIZE_MAX    by %%Iu = %Iu\n", size_t(SIZE_MAX)); // ターゲット プラットフォーム依存。

// %Id, %Iu はそれぞれ ptrdiff_t, size_t 相当の型(ポインタ互換整数型)に対して使う。
// %ld, %lu はそれぞれ signed long, unsigned long 用。Win32/Win64 では int と long のサイズが同じなので %d, %u と違いはないが、Win16 では区別される。
// signed long long および unsigned long long にはそれぞれ %lld, %llu を使う。Win32/Win64 では 64bit 整数になる。

int32_tやint64_tなどはC99やC++11 (C++0x TR1) の <stdint.h>, <cstdint> で定義されている、処理系ごとにサイズ保証された型なんですが、VC++では2010以降で実装されています。

ちなみにWindows APIでよく見かけるINT_PTR、UINT_PTR、LONG_PTR、ULONG_PTR、DWORD_PTRなども、単に「ポインタと相互変換してもいい整数型(アドレスを格納できるだけの幅を持った整数型)」という意味のプラットフォーム依存型なので注意しましょう。.NETでいうとSystem.IntPtrSystem.UIntPtrに相当します。C99ではオプションとして intptr_t, uintptr_t という型が標準化されていますが、これらも同じです。

まぁ16進数表記でかまわないんであれば、size_tもptrdiff_tも従来の標準規格に沿った%p書式指定を使って出力すればよいです。

あと、こういう環境依存の型に対する操作は、C++のストリームのほうが本質的に型安全で、楽といえば楽です。C/C++で可変引数(可変長引数、可変個引数)を使う場合、うかつに変数の型を変えられないというデメリットがあります(必ず対応する書式も注意深く修正する必要があります*1)。

Boost C++ライブラリが使える環境では、Boost.Formatを使うという手もあります。

なお、gccでは%Id, %Iuの代わりに%zd, %zu, %td, %tuなどの書式が使えるようです。これらはC99規格の書式らしいです。

他にも、C99では処理系における最大サイズの整数を格納できる intmax_t, uintmax_t が定義されています。対応する長さ修飾子はjで、それぞれ%jd, %juを使います。
ユーザー定義のtypedef整数型エイリアスは、具体的な型が何に展開されるか定まっておらず、処理系が変わると書式と適合しなくなるおそれがあるので、printf()/scanf()系の引数に直接渡すべきではありません。
short, int, longなどの組み込み型もしくは標準規格で定義されているエイリアス型を使うべきです。

C99では signed char / unsigned char を出力するための長さ修飾子hhも追加されています。C/C++において、int未満のサイズを持つ型の値を可変引数に渡すと、「既定の実引数拡張」のルールによっていったんsigned intもしくはunsigned intに型昇格されるのですが、このため負数を16進数で出力する場合は注意が必要となります。C99より前の規格でも、signed shortに関しては長さ修飾子hがあったので特に問題なかったのですが、signed charに関するサポートがなく、いったんunsigned charにキャストしてから可変引数に渡す*2などの面倒な対処が必要でした。

printf("SCHAR_MIN = 0x%x\n", static_cast<signed char>(SCHAR_MIN)); // 大抵の処理系で 0xffffff80 になる。
printf("SCHAR_MIN = 0x%x\n", static_cast<unsigned char>(SCHAR_MIN)); // 大抵の処理系で 0x80 になる。
printf("SCHAR_MIN = 0x%hhx\n", static_cast<signed char>(SCHAR_MIN)); // 大抵の処理系で 0x80 になる。

既定の実引数拡張に関しては以下も参照してください。

MSVCがなぜC99をサポートしないのか、なぜ独自拡張にこだわるのか、詳しいことはよくわからないんですが、64bit版WindowsLinux系とは違ってLLP64モデルであること、Windowsネイティブ開発における事実上標準言語がC++であることに起因しているような気がします。C言語オープンソース界隈とかは、MSVCがC言語のサポート強化を怠けてる状況に憤りをおぼえているかもしれません(クロスプラットフォームなライブラリを作る際は一番遅れている処理系に合わせた規格でコードを書かないといけないので、あるOS上での事実上標準の処理系が他に遅れをとっていると非常に困る)。
しかし、C言語によるコード生産性はすでに他の言語と比べると大きく遅れをとっており、せいぜいアセンブラよりはマシといった程度でしかないので、WindowsプログラマーにとってC++(もしくはC#)の修得は必須ですが、Cはほとんど利用する場面がありません(ドライバー開発は除く)。それゆえ、MSにとってCはもはや死んだ言語同然なのだと思われます。ちなみに個人的には

C#の文法>>C++の文法>>>>>>>>>>Cの文法」

くらいの差があると考えています。文法仕様の品質・完成度は言語機能同様、コードの生産性やメンテナンス性に大きく影響を及ぼします。

※2020-11-24追記:
VC2013までは hh, t, z, j の長さ修飾子が実装されていませんでしたが、VC2015以降は実装されているようです。
ATLTRACE()CString::Format() でも使えます。

C++11規格では標準ライブラリの大半がC99互換になっています。

*1:学生の頃、16bit環境向けプログラムの変数の型を一部intからlongに変えてサイズ拡張する作業をやらせてもらったことがありましたが、そのときに対応する書式をきちんと変えなかったため、データが正常に出力されなくなるバグを混入させてしまったことがあります。

*2:この方法は、2の補数系でない場合は通用しないかもしれません。