[C++] パラメータパックをprint/formatする

printfとその仲間たちに関する記事ではありません

フォーマット文字列とパラメータパック

std::format()およびstd::print()のフォーマット文字列は共通のもので、文字列内の{}(置換フィールド)に対して、与えられた引数(フォーマット対象の値)を文字列化したものを順番に置き換えていきます。

// フォーマット文字列の例
std::print("{}", 0);
std::print("{}{}{}", 0, 1.0, "2");

この時、この対応関係はコンパイル時にチェックされ、フォーマット文字列の{}の数に対して引数が足りない場合はエラーになります(なぜか、多いだけならエラーにはならないようです)。

std::print("{}{}{}", 0);      // ng
std::print("{}", 0, 1, 2, 3); // ok

引数の値は定数である必要はなく実行時の値であっても当然問題ないのですが、フォーマット文字列そのもの、およびそれに現れている置換フィールド{}の数とフォーマット対象の引数の数は、コンパイル時にコード上で一致している必要があります。

どちらも非常に便利でありC++20/23以降は手放せなくなること必至でしょう。それを自然に使用していると、パラメータパックに対して使用したくなる時が来るでしょう、いずれ。しかし、パラメータパックのその独特な性質により一筋縄ではいかないことに気づきます。

void any(auto&&...);

void f(auto&&... args) {
  // コンパイル時に要素数は取れる
  constexpr std::size_t N = sizeof...(args);

  // パックそのものは他に渡せない
  any(args);    // ng
  any(args...); // ok

  // パラメータパックそのものには型は無い
  using t = decltype(args); // ng
}

パラメータパックそのものに対してはサイズを取得することと展開することくらいしかできず、取りまわすようなことは不可能です。そのため、std::format()/std::print()でも、パラメータパックそのものを渡して文字列化することはできません。

void print_pack(auto&&... args) {
  std::print("{}", args);     // ng

  // パックの先頭の値のみが出力される
  std::print("{}", args...);  // ok(パックが空でなければ)
}

多い分には問題ないので先頭N個だけ出力したければこういう感じでもいいのかもしれませんが、パラメータパックに含まれる値をすべて出力したい場合に、フォーマット文字列をべた書きすることはできず、なんらか生成する必要が出てきそうに思えます。

コンパイル時生成

C++17以前だとメタメタプログラミングの世界へようこそ!するのですが、C++20であればstd::stringコンパイル時に使用できるのでいくらからくにはなります。

#include <ranges>
#include <print>
#include <string>

template<std::size_t N>
consteval auto make_format_string_for_pack() {
  std::string fmtstr = "";
  
  // デリミタはお好みで
  constexpr std::string_view append = "{}, ";
  
  for (auto _ : std::views::iota(0ull, N)) {
    fmtstr += append;
  }

  std::array<char, append.length() * N + 1> save;
  std::ranges::copy(fmtstr, save.begin());
  save.back() = '\0';

  return save;
}

void print_pack(auto&&... args) {
  static constexpr auto fmtstr_array = make_format_string_for_pack<sizeof...(args)>();
  std::print(fmtstr_array.data(), args...);
}

int main() {
  print_pack(1, "str", 3.1415, 'c', true);
}
1, str, 3.1415, c, true, 

これでもいいんですが、書くことが多いし、かなりテクニカルな部分が多いコードになっています。なぜわざわざstd::arrayに詰めなおしているのか?なぜstatic constexpr??などなど。

実行時フォーマットを使う

素直に実行時にやる方法です。

#include <ranges>
#include <print>
#include <string>

void print_pack(auto&&... args) {
  std::string fmtstr = "";
  
  for (auto _ : std::views::iota(0ull, sizeof...(args))) {
    // デリミタはお好みで
    fmtstr += "{}, ";
  }
  
  std::vprint_unicode(fmtstr, {std::make_format_args(args...)});
}

int main() {
  print_pack(1, "str", 3.1415, 'c', true);
}
1, str, 3.1415, c, true, 

記述量自体は減りましたし実装も単純になりました。しかしこの場合、フォーマット文字列のチェックがコンパイル時に行われなくなるため、例えばパックの中にフォーマットできない型が含まれていても実行時エラーになります。

std::tie()

古来より、パラメータパックを扱う際にはstd::tupleとその周辺ユーティリティが便利に活用できることが良く知られています。そして、C++23からはstd::tupleのフォーマットサポートが入るため、std::tupleはそのままフォーマット可能になります。

したがって、tuplestd::tie())を利用することで一番シンプルに書くことができます。

#include <ranges>
#include <print>
#include <string>

void print_pack(auto&&... args) {
  std::print("{}", std::tie(args...));
}

int main() {
  print_pack(1, "str", 3.1415, 'c', true);
}
(1, "str\u{0}", 3.1415, 'c', true) 

文字列出力がエスケープされているのはたぶんclangの実装バグです。本来エスケープされないのが正しいはず。

一番短くかつ単純でありながら、この方法であれば非パック引数と組み合わせることもできるしstd::tuple用のフォーマットオプションを使用することもできます。

ただし、std::tupleのフォーマットサポートがC++23からなので、C++20のstd::format()ではこの方法は使えません。前の2つのどちらかで頑張りましょう。

なお、std::tie(args...)ではなくstd::tuple(args...)を使用しても同じことは達成できますが、tupleを直接構築してしまうと各要素がコピーされるので注意が必要です。

std::tupleのフォーマットオプション

std::tie()による方法であれば、限定的なものではあるもののstd::tupleのためのフォーマットオプションを使用することができます。

まず、nオプションによって囲み文字(())を省くことができます。

void print_pack(auto&&... args) {
  std::println("{:n}", std::tie(args...));
  std::println("{{{:n}}}", std::tie(args...));
}
1, "str\u{0}", 3.1415, 'c', true
{1, "str\u{0}", 3.1415, 'c', true}

std::tuple固有のオプションはこれくらいで、残りは整数型等他の型と同じく幅指定に関するオプションが使用できます。

void print_pack(auto&&... args) {
  std::println("|{:*<40n}|", std::tie(args...));
  std::println("|{:+>40n}|", std::tie(args...));
  std::println("|{:-^40n}|", std::tie(args...));
}
|1, "str\u{0}", 3.1415, 'c', true********|
|++++++++1, "str\u{0}", 3.1415, 'c', true|
|----1, "str\u{0}", 3.1415, 'c', true----|

幅と寄せ、埋め文字のオプション指定はtupleの要素型毎ではなくtupleの全体に適用されます。

参考文献

この記事のMarkdownソース