C++20では、ついに新しい文字列フォーマットライブラリが導入されます。
今のC++標準ライブラリはCから引き継いだprintf
系の関数群と、C++で導入されたiostream
という2つのフォーマットライブラリを持っています。
printf
系関数には多くの問題があり、できればC++で使いたくはないものです1。
- 型安全ではない(Cの可変長引数そのものが)
- 書式ではなく、変数の型を指示する書式指定がたくさんある (
%d
と%ld
など) - 標準ではユーザー定義型へ拡張できない
- 書式文字列は実装による差が大きい
一方でprintf
スタイルには以下の利点もあり、C++より後発の言語でも書式文字列を使ったフォーマットは広く採用されています。
- 書式と引数がはっきり分かれている
- 普通の関数呼び出しとして理解可能
- 多くのプログラマーが慣れている
C++20のフォーマットライブラリ(<format>
)は書式文字列の分かりやすさとiostream
の拡張性・型安全性の両方を備えたライブラリであり、一言で言えば**現代版printf
**です。
- C#, Python, Rustなどで実績がある、
{}
で引数を展開する新しい書式文字列 - フォーマットできる型、書式文字列の両方が拡張可能
- アライメントや2進数などフォーマット自体の機能も強化
string message = format("The answer is {}.", 42);
std::format
は規格化と並行して{fmt}ライブラリで実装が進められていて、すでに広く利用されています。
使い方
以下の4つのフォーマット関数と、それらのwstring_view
を引数とするオーバーロードが提供されます。u(8|16|32)string
への対応は無いようです。
この他に、可変長引数を型消去した内部実装用の関数も公開されていますが、通常は使う必要はありません。
format
template<class... Args>
string format(
string_view fmt,
const Args&... args);
書式文字列fmt
を使って引数args...
をフォーマットした文字列を返す。
fmt
が書式文字列でない場合は format_error
を投げる。
string message = format("The answer is {}.", 42); // => "The answer is 42."
format_to
template<class Out, class... Args>
Out format_to(
Out out,
string_view fmt,
const Args&... args);
書式文字列fmt
を使って引数args...
をフォーマットし、出力イテレーターout
に出力する。
out + N
を返す(N == formatted_size(fmt, args...)
)。
fmt
が書式文字列でない場合は format_error
を投げる。
string buffer;
format_to(back_inserter(buffer), "The answer is {}.", 42);
cout << buffer; // The answer is 42.
format_to_n
template<class Out, class... Args>
format_to_n_result<Out> format_to_n(
Out out,
iter_difference_t<Out> n,
string_view fmt,
const Args&... args);
template<class Out>
struct format_to_n_result {
Out out;
iter_difference_t<Out> size;
};
書式文字列fmt
を使って引数args...
をフォーマットし、最大でn
文字を出力イテレーターout
に出力する。
{ out + M, N }
を返す(N == formatted_size(fmt, args...)
、M == min(max(n, 0), N)
)。
fmt
が書式文字列でない場合は format_error
を投げる。
char buffer[256];
auto [end, n] = format_to_n(buffer, size(buffer)-1, "The answer is {}.", 42);
*end = '\0'; // null文字は出力されない
cout << buffer; // The answer is 42.
formatted_size
template<class... Args>
size_t formatted_size(
string_view fmt,
const Args&... args);
書式文字列fmt
を使って引数args...
をフォーマットした文字列の長さを返す。
fmt
が書式文字列でない場合は format_error
を投げる。
書式
書式文字列中では、{
と}
で囲まれた範囲が置換フィールドとなります(エスケープシーケンスは{{
と}}
)。
置換フィールドの書式は次の通りです([]
は省略可の意味)。
{ [引数ID] [: オプション] }
- 引数IDは0から始まる番号で、何番目の引数で置換するかを指定します。
- 引数IDを一部のフィールドだけに書くことはできません(すべての置換フィールドに指定するか、すべての置換フィールドで指定しないかのどちらかのみ)。
- オプションの書式は引数の型によって異なります。
標準のオプション書式
組み込みの型に対して使える標準のオプション書式は次の通りです([]
は省略可の意味)
基本的にprintfの書式を踏襲しています。
オプションが無くても<iostream>
と同じようにいい感じのデフォルト書式が使われます。
[[fill] align] [sign] ['#'] ['0'] [width] ['.' precision] [type]
-
fill
: アライメントに使う文字 (デフォルト: スペース) -
align
: アライメント(デフォルトは型による)-
>
: 右寄せ -
<
: 左寄せ -
^
: 中央寄せ -
=
: 符号の後にアライメントをする
-
-
sign
: 符号-
+
: 正の数でも符号を表示する -
-
: 負の数の場合のみ符号を表示する(デフォルト) -
-
-
#
: 代替表現(0x
など形式がわかる表記)を使う -
0
: 符号を考慮して0で埋める (fill
を0
、align
を=
にした場合と同じ) -
width
: 幅 (省略時は値に応じて幅が決まり、アライメントは機能しない)- 置換フィールドを使って変数で指定できる
-
precision
: 精度(浮動小数点数の場合)、使う文字数(文字列の場合)- 置換フィールドを使って変数で指定できる
-
type
: 値の表現方法(表を参照)
文字列型の場合
type | 意味 |
---|---|
s (省略可) | 文字列をprecision 文字だけ出力する |
文字型の場合
type | 意味 |
---|---|
c (省略可) | 1文字 |
整数型の場合
type | 意味 |
---|---|
b | 2進数(小文字) |
B | 2進数(大文字) |
o | 8進数 |
d (省略可) | 10進数 |
n | 10進数(ロケールを考慮する) |
x | 16進数(小文字) |
X | 16進数(大文字) |
浮動小数点数型の場合
type | 意味 |
---|---|
f,F | 指数表記しない |
e | 指数表記(小文字) |
E | 指数表記(大文字) |
a | 16進指数表記(小文字) |
A | 16進指数表記(大文字) |
g (省略可) | 値に応じてf またはe
|
G | 値に応じてF またはE
|
n | ロケールを考慮して値に応じて指数表記する |
ポインターの場合
すべてのポインター型とnullptr_t
(nullptr
の型)が該当します。
type | 意味 |
---|---|
p (省略可) | アドレスを出力する (uintptr_t にキャストして小文字16進数で出力) |
引数の型変換
charT
をフォーマッターの文字型とします。
- 書式文字列が
string_view
の場合はchar
、wstring_view
の場合はwchar_t
format系関数の可変長引数に渡した値の1つをconst T& v
で受け取るとすれば、変換後の値は次の通りです。
-
T
がbool
,charT
,charT*
,double
,long double
の場合はそのまま -
T
がchar
でcharT
がwchar_t
の場合はstatic_cast<wchar_t>(v)
-
T
が符号ありの整数型の場合- 大きさが
int
以下の場合はstatic_cast<int>(v)
- 大きさが
long long int
以下の場合はstatic_cast<long long int>(v)
- 大きさが
-
T
が符号なしの整数型の場合- 大きさが
unsigned int
以下の場合はstatic_cast<unsigned int>(v)
- 大きさが
unsigned long long int
以下の場合はstatic_cast<unsigned long long int>(v)
- 大きさが
-
T
がfloat
の場合はstatic_cast<double>(v)
-
T
がbasic_string_view<charT>
に変換できる場合はbasic_string_view<charT>(v)
-
T
がnullptr_t
の場合はstatic_cast<const void*>(nullptr)
-
T
がポインターの場合はstatic_cast<const void*>(v)
- それ以外の場合は
v
のアドレスが保存され、ユーザー定義フォーマッターが使われる
Tips
- ロケールを考慮しない場合、算術型の出力はstd::to_charsで行われます。
例(P0645R10より)
char c = 120;
format("{:6}", 42); // " 42"
format("{:6}", 'x'); // "x "
format("{:*<6}", 'x'); // "x*****"
format("{:*>6}", 'x'); // "*****x"
format("{:*^6}", 'x'); // "**x***"
format("{:=6}", 'x'); // エラー
format("{:6d}", c); // " 120"
format("{:=+06d}", c); // "+00120"
format("{:0=#6x}", 0xa); // "0x000a"
format("{:6}", true); // "true "
double inf = numeric_limits<double>::infinity();
double nan = numeric_limits<double>::quiet_NaN();
format("{0:} {0:+} {0:-} {0: }", 1); // "1 +1 1 1"
format("{0:} {0:+} {0:-} {0: }", -1); // "-1 -1 -1 -1"
format("{0:} {0:+} {0:-} {0: }", inf); // "inf +inf inf inf"
format("{0:} {0:+} {0:-} {0: }", nan); // "nan +nan nan nan"
format("{}", 42); // "42"
format("{0:b} {0:d} {0:o} {0:x}", 42); // "101010 42 52 2a"
format("{0:#x} {0:#X}", 42); // "0x2a 0X2A"
format("{:n}", 1234); // "1,234" (ロケールによる)
拡張性
書式文字列と使える型の両方が拡張可能です。
拡張するには、クラステンプレートformatter
を特殊化します。
ostream
との間にoperator<<
を定義することでストリームに対応できるのと同じように、formatter
を特殊化すると自動的に使ってくれます。
// std::tmに対する特殊化の例
template<>
struct formatter<tm> {
constexpr format_parse_context::iterator parse(format_parse_context& ctx);
template<class FormatContext>
typename FormatContext::iterator format(const tm& tm, FormatContext& ctx);
};
time_t t = time(nullptr);
string date = format("The date is {0:%Y-%m-%d}.", *localtime(&t));
-
parse
関数は書式文字列のうち、対応するフィールドのフォーマット仕様({}
内の:
から}
までの部分)の解析を行います。なお、parse
関数がconstexpr
である場合、書式文字列はコンパイル時に解析される可能性があります。 -
format
関数は値をフォーマットし、イテレーター(ctx.begin()
)に出力します。
オプションの値などの情報はフォーマッターのメンバー変数に保存することができます。
次のコードは組み込みオプションの width
を再現し、幅を変数で指定できるカスタムフォーマッターの実装例です。
// P0645R10から引用
struct Answer {};
template<>
struct formatter<Answer> {
int width_arg_index = 0;
// Parses dynamic width in the format "{<digit>}".
auto parse(format_parse_context& parse_ctx) {
auto iter = parse_ctx.begin();
auto get_char = [&]() { return iter != parse_ctx.end() ? *iter : 0; };
if (get_char() != '{')
return iter;
++iter;
char c = get_char();
if (!std::isdigit(c) || (++iter, get_char()) != '}')
throw format_error("invalid format");
width_arg_index = c - '0';
return ++iter;
}
auto format(Answer, format_context& format_ctx) {
auto arg = format_ctx.arg(width_arg_index);
int width = visit_format_arg([](auto value) -> int {
if constexpr (!std::is_integral_v<decltype(value)>)
throw format_error("width is not integral");
else if (value < 0 || value > std::numeric_limits<int>::max())
throw format_error("invalid width");
else
return value;
}, arg);
return format_to(format_ctx.out(), "{:{}}", 42, width);
}
};
std::string s = format("{0:{1}}", Answer(), 10);
// s == " 42"
ここで、format_parse_context
はそれ自身が char
のrangeになっています。
パフォーマンス
P0645R10によればprintf
、ostringstream
、to_string
より速いようです。
// P0645R10から引用
Run on (4 X 3100 MHz CPU s)
2018-01-27 07:12:00
Benchmark Time CPU Iterations
----------------------------------------------------
sprintf 882311 ns 881076 ns 781
ostringstream 2892035 ns 2888975 ns 242
to_string 1167422 ns 1166831 ns 610
format 675636 ns 674382 ns 1045
format_to 499376 ns 498996 ns 1263
速くなる要因としてはロケール依存の処理をしないことが考えられます。
また、コードサイズが増えすぎないようにthin-templateを使っていて、可変長引数の部分は型消去して別の構造体に詰めています。実際にはformat_to
は次のように定義されます。
template<class Out, class... Args>
Out format_to(Out out, string_view fmt, const Args&... args) {
using context = basic_format_context<Out, decltype(fmt)::value_type>;
return vformat_to(out, fmt, {make_format_args<context>(args...)});
}
さらに詳しく
謝辞
この記事はC++20を相談しながら調べる会 #3の結果として書かれました。