📏

std::printf 系関数の length modifier

2024/02/20に公開

はじめに

C++ の std::printf 系関数の書式指定文字列の length modifier のメモです。

std::printf 系の関数は知名度は高いと思いますが、書式指定を誤っているケースをちらほら見かけます。
この記事では、書式指定文字列中の length modifier のうち、さらに一部に特化して言及します。

なお、書式指定文字はすべて覚える必要はなく、その都度 cppreference.com[2] などを確認すればよいと思います。 テストではないので。

length modifier とは

length modifier[3] とは引数のサイズを指定する文字のことです。

まずは例を示します。
std::printf 関数で long (signed long int) 型の値を 10 進数表現で出力するには次のようにしますね:

Code 1
signed long int value{/* 適当な値 */};
std::printf("%ld", value);

第 1 引数の書式指定文字列 "%ld" のうち、 llength modifier です (ちなみに dconversion specifier です)。
この length modifier l は、続く第 2 引数 value の型のサイズが long であることを std::printf 関数に伝えます。

length modifier として用意されているのは C++23 時点では全部で次の 9 種類です: hh, h, (なし), l, ll, j, z, t, L

これらの文字の意味は後ろに続く conversion specifier によって変わります。
conversion specifier は先の Code 1 の例における "%ld" のうちの d です。

たとえば、符号つき整数に適用して 10 進数表現で書式化する conversion specifier d の場合、 length modifier を指定しない (つまり "%d") と引数は int と解釈されますが、 length modifier ll (小文字のエル 2 つ) をつける (つまり "%lld%") と long long int と解釈されるようになります (Table 1)。

Table 1. length modifierconversion specifier d への適用例

length modifier 書式指定文字列 期待される引数の型
hh "%hhd" signed char
h "%hd" short (signed short int)
(なし) "%d" int (signed int)
l "%ld" long (signed long int)
ll "%lld" long long (signed long long int)

プロモーション

前の節でさまざまな length modifier をご紹介しましたが、「hhh なんて使ったことないよ, 使わなくても困ったことないよ」という方もいらっしゃると思います。
そのあたりの事情を簡単にご説明します。

突然ですが、 C++ にはプロモーション (promotions; 昇格) という仕様があります。
プロモーションには整数のプロモーション (integral promotions)[4] と浮動小数点数のプロモーション (floating-point promotion)[5] があります。

簡単に説明すると、前者の代表的なものは int 型より小さい整数型の値は int 型の値に変換されるというものです。
後者は float 型の値は double 型の値に変換されるものです。
簡単のため、以降は前者; 整数のプロモーションに限定して話を進めます。

このプロモーションによって、 signed char 型や short 型の値を std::printf 関数に渡した場合、それらはいずれも int 型に変換されてから渡されます。

length modifier hhh は、 int 型に変換された値を変換前の型に戻してから書式化することを指示します。

length modifier hh の意味の説明[6]

Specifies that a following b, d, i, o, u, x, or X conversion specifier applies to a signed char or unsigned char argument (the argument will have been promoted according to the integer promotions, but its value shall be converted to signed char or unsigned char before printing); (略)

ただし、 signed char 型や short 型の値は int 型で表現できますので、それらを "%d" で (int 型のまま) 書式化しても特に問題となることはないと思います。

std::size_t 型

std::size_t は、 <cstddef> などのヘッダで定義される型です。
規格では次のように規定されています[7]:

The type size_t is an implementation-defined unsigned integer type that is large enough to contain the size in bytes of any object.

std::size_t 型は sizeof 演算子の結果の型であるほか、標準ライブラリの各種コンテナの size メンバ関数が返す型でもあり、コード中での出番は多いです。

同型のサイズは先の規定のように主に (ターゲットの) CPU アーキテクチャに合わせて決められていて、 4 Bytes の環境もあれば 8 Bytes の環境もあり、まちまちです。
一方で int などの基本型 (組み込み型) のサイズはそれとは別の事情によって決められているため、 std::size_t 型の値を "%u" (unsigned int 用) や "%lu" (unsigned long 用) などで出力すると移植性が低下してしまいます。

たとえば、 64 bit の環境 (x86-64 や AArch64 など) では std::size_t 型のサイズは一般には 8 Bytes です。
それに対して基本型 (組み込み型) はというと、たとえば long (long int) のサイズは Linux では 8 Bytes ですが Windows (Win64) では 4 Bytes と異なります[8]
std::size_t 型の値を 10 進数で出力する場合、 Linux では "%lu" (unsigned long 用) でもかまいませんが、 Windows では "%lu" では足りず "%llu" (unsigned long long 用) とする必要があります。
こういったことを考慮すると、ポータブルなコードを書くのは大変なことのように思えますね。

そこで、 std::size_t 型には専用の length modifier z が用意されています。
(同型は符号なしですから) 10 進数表現で文字列化する場合には "%zu" とすればよいです。

固定長整数型

C++ ではほとんどの基本型 (組み込み型) のサイズは規定されていませんので、サイズを明示的に指定したい場合には、 std::int32_t をはじめとした <cstdint> で定義されている固定長整数型 (fixed width integer types)[10] を使うこともあるでしょう。

たとえば、 std::int32_t 型は、ある環境では int かもしれませんし、別のある環境では long かもしれません。
この型の値を std::printf 関数で文字列化する場合、 int 型なら書式指定は "%d" ですが、 long 型であれば "%ld" とする必要があります。
移植性を確保したまま固定長整数型を std::printf 系関数で文字列化するにはどうしたらよいのでしょうか。

実は、そのためのマクロが <cinttypes> に用意されています。
たとえば std::int32_t 型であれば PRId32 です。
このマクロは length modifierconversion specifier を表す文字列に展開されるので、これの前に "%" (と必要に応じて他の指定子) を書いてあげます。
他の型用のマクロは参考文献 4 をご覧ください。

参考文献

  1. std::printf, std::fprintf, std::sprintf, std::snprintf - cppreference.com.
    https://en.cppreference.com/w/cpp/io/c/fprintf
  2. N3054: Programming languages ⸺ C, working draft.
  3. N4950: Working Draft, Standard for Programming Language C++.
  4. Fixed width integer types (since C++11) - cppreference.com.
    https://en.cppreference.com/w/cpp/types/integer
  5. 64ビット - Wikipedia.
    https://ja.wikipedia.org/wiki/64ビット
脚注
  1. 組み込みシステムでは標準出力がない (あるいは使わない) こともめずらしくありませんが、 std::sprintf 関数経由でログに出力する場合などに用いられていると思います。 ↩︎

  2. 参考文献 1 ↩︎

  3. 参考文献 2 の 7.23.6.1 ↩︎

  4. 参考文献 3 の 7.3.7 [conv.prom] ↩︎

  5. 参考文献 3 の 7.3.8 [conv.fpprom] ↩︎

  6. 参考文献 2 の 7.23.6.1 ↩︎

  7. 参考文献 3 の 17.2.4 [support.types.layout] ↩︎

  8. 参考文献 5 ↩︎

  9. 正しくはサフィックス u または U と併用すると std::size_t 型と解釈され、 z または Z 単独では符号つきの std::size_t 型となります。 ↩︎

  10. 参考文献 4 ↩︎

Discussion