53
Help us understand the problem. What are the problem?

posted at

updated at

C++20の文字列フォーマットライブラリ std::format

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で埋める (fill0align=にした場合と同じ)
  • 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の場合はcharwstring_viewの場合はwchar_t

format系関数の可変長引数に渡した値の1つをconst T& vで受け取るとすれば、変換後の値は次の通りです。

  • Tbool,charT,charT*,double,long doubleの場合はそのまま
  • TcharcharTwchar_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)
  • Tfloatの場合はstatic_cast<double>(v)
  • Tbasic_string_view<charT>に変換できる場合はbasic_string_view<charT>(v)
  • Tnullptr_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によればprintfostringstreamto_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の結果として書かれました。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
53
Help us understand the problem. What are the problem?