C++20期待の新機能std::formatを試してみる

ついにVisual Studio 2019(ver 16.10)でC++20の新機能、`std::format`が使えるようになったということなので、試してみました。結論から言うと非常に満足いくものであり、`sprintf`はもう使わなくてもいいかな、という気持ちです。

# 前置き

C++で書式つき文字列を扱う場合、`printf`や`sprintf`といったC言語の関数か、あるいはC++の`iostream`を使うという2通りのやり方があります。

`printf`の場合、引数の数や型をチェックしてくれないという点や`std::string`と親和性がない問題が、そして`iostream`の場合は使い方が独特(<<, >>といった演算子、内部状態を持つ)という問題があります。

`printf`の手軽さと、`iostream`の型安全性・拡張性を兼ね備えた[boost::format](https://www.boost.org/doc/libs/release/libs/format/)のようなライブラリも登場しましたが、やはり標準ライブラリに入っていないと導入が大変でした(最近ようやくC++界隈でもパッケージマネージャが広まってきましたが)。

そんなわけで、C++20にて新しい書式つき文字列のためのライブラリ`format`が導入されると聞き、私のように期待していたC++開発者も多いのではないでしょうか。

そして、2021年5月末、ようやくMicrosoftがVisual Studio 2019(16.10)にて`format`の実装をリリースしました。`GNU`(libstdc++)や`Clang`(libcxx)より先に出してくるとは思っていませんでした。近年のMicrosoftは本当にすごい。

[Visual Studio 2019 バージョン16.10 のリリースノート](https://docs.microsoft.com/ja-jp/visualstudio/releases/2019/release-notes#16.10.0)
[libstdc++のC++20開発状況](https://gcc.gnu.org/onlinedocs/libstdc++/manual/status.html#status.iso.2020) -> P0645をページ内検索
[libcxxのC++20開発状況](https://libcxx.llvm.org/docs/Cxx2aStatus.html) -> P0645をページ内検索 

# 導入

特に難しいことは何もないので、詳しくは解説しません。

1. Visual Studio 2019のアップデート
    - Visual Studio 2019を持っていない方
        - [Visual Studio 2019](https://visualstudio.microsoft.com/ja/vs/)をインストールします(どのEditionでもOK)
    - Visual Studio 2019を持っている方
        - `Visual Studio Installer`を実行し、2019を最新にアップデートします。
        - その後`Visual Studio 2019`を起動し、バージョンが`16.10.0`以降になっていればOK

2. C++のプロジェクトを作成
    - プロジェクトの種類はなんでもいいです(私は「コンソールアプリ」で試しています)

3. C++の機能スイッチを`/std:c++latest`に設定する
    - プロジェクトの「プロパティ」を開く
    - 左側から「全般」を選択
    - 「C++ 言語標準」を「プレビュー - 最新の C++ Working Draft からの機能 (/std:c++latest)」に設定
    - ※ソリューション構成(Release/Debug)とプラットフォーム(x86/x64)を間違えないように注意

本来だと`/std:c++latest`ではなく、`/std:c++20`の機能スイッチが提供される予定でしたが、バイナリレベルでの仕様(ABI)がまだ安定していないということで、実装されていません。`/std:c++latest`はぶっちゃけ人柱用の設定であり、ビルドしたバイナリの互換性はないことに注意してください。


# 基本

ライブラリ`format`をインクルードするだけで使用できます。

```cpp
#include <iostream>
#include <format>

int main()
{
	using namespace std;
	cout << format("Hello {}!", "std::format") << endl;
}
```
```
Hello std::format!
```

# 使い方

`std::format`による書式のスタイルは、今まで使用してきた`printf`や`iostream`とも異なる表記を用います。`C#`の[System.Console.WriteLine](https://docs.microsoft.com/ja-jp/dotnet/api/system.console.writeline?view=net-5.0)や`Python`の[f-strings](https://docs.python.org/ja/3.8/tutorial/inputoutput.html#tut-f-strings)、`Rust`の[format](https://doc.rust-lang.org/std/fmt/)に近しいもので、これらを使ったことがある人ならすぐに使いこなすことができるでしょう。

書式文字列の中に変数を差し込みたい場合、カーリーブラケット`{}`を使用してその場所を指定します。

詳しい使い方についてはすでにQiitaに記事があるので、そちらを参照してください。

[C++20の文字列フォーマットライブラリ std::format](https://qiita.com/tetsurom/items/e660c7aaf008d8a1e904)

# 例外

以下では上記の記事では触れていないポイントについて簡単に解説していきます。

`std::format`では書式や引数が不正な場合、実行時例外として`std::format_error`を発生させます。

```cpp
try {
	// 1個しか引数がないのに、2個目を使用しようとしてみる
	cout << format("{1}", 0) << endl;
}
catch (const format_error& e)
{
	cout << e.what() << endl;
}
```
↓

```
Argument not found.
```

書式文字列が不正な場合も例外が発生します。

```cpp
format("{$324i0-g}", 0)
Invalid format string.
```

書式内で指定した型と引数の型が違う場合も例外が発生します。

```cpp
format("{:d}", 4.0f)
invalid floating point type
format("{:f}", 42)
invalid integral type
```

`fill`指定に`{`や`}`を使うとエラーになります(これ以外の文字は使える)。

```cpp
format("{:{>10}", 42)
invalid fill character '{'
format("{:}>10}", 42)
Unmatched '}' in format string.
```

浮動小数点数以外で`precision`指定はできません。

```cpp
format("{:6.3}", 42)
Precision not allowed for this argument type.
```

# ワイド文字列

もちろん`wchar_t`や`wstring`に対応しているので、日本語も扱えます。

```cpp
#include <iostream>
#include <format>
#include <locale>

int main()
{
	using namespace std;
	setlocale(LC_CTYPE, "");
	wcout << format(L"{}", L"こんにちは！") << endl;
}
```

```
こんにちは！
```

`fill`指定にもワイド文字は使えますが、期待した動作にはならないようです。

```cpp
wcout << format(L"{:あ^20}", L"中央寄せ") << endl;
ああああああ中央寄せああああああ  // 'あ'が幅1として扱われている？(2*4+12=20)
```

まあ、個人的には正直C/C++で日本語は扱いたくないなあと思っているのでどうでもいい気も……。

# 書式指定チートシートもどき

|説明|記述|結果|
|---|---|---|
|1個|format("{}", 1)|1|
|2個|format("{} {}", 1, 2)|1 2|
|3個|format("{} {} {}", 1, 2, 3)|1 2 3|
|順序指定|format("{2} {1} {0}", 1, 2, 3)|3 2 1|
|文字(cは省略可)|format("{:c}", 'C')|C|
|文字列(sは省略可)|format("{:s}", "C++")|C++|
|bool|format("{}", true)|true|
|bool|format("{}", false)|false|
|整数(10進数,dは省略可)|format("{:d}", 42)|42|
|整数(8進数)|format("{:o}", 042)|42|
|整数(16進数)|format("{:x}", 0xab)|ab|
|整数(16進数) 大文字|format("{:X}", 0xab)|AB|
|整数(16進数) 0x付き|format("{:#x}", 0xab)|0xab|
|整数(16進数) 0X付き|format("{:#X}", 0xab)|0XAB|
|整数(2進数)|format("{:b}", 0b10101010)|10101010|
|整数(2進数) 0b付き|format("{:#b}", 0b10101010)|0b10101010|
|整数(2進数) 0B付き|format("{:#B}", 0b10101010)|0B10101010|
|整数(正数に+)|format("{:+d}", 42)|+42|
|浮動小数点数|format("{:f}", 123.456789)|123.456789|
|精度指定|format("{:6.3f}", 123.456789)|123.457|
|指数表記(e)|format("{:e}", 123.456789)|1.234568e+02|
|指数表記(E)|format("{:E}", 123.456789)|1.234568E+02|
|最適な表記を自動判定<br>(gは省略可)|format("{:g}", 123.456789)|123.457|
|左寄せ|format("{:<12}", "left")|left&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|
|右寄せ|format("{:>12}", "right")|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;right|
|中央寄せ|format("{:^12}", "center")|&nbsp;&nbsp;&nbsp;center&nbsp;&nbsp;&nbsp;|
|埋める文字を指定する|format("{:!^12}", "hello")|!!!hello!!!!|

# 参考文献

[Text Formatting - www.open-std.org](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0645r10.html): C++標準化委員会による仕様
[format - cpprefjp - C++日本語リファレンス](https://cpprefjp.github.io/reference/format.html): 広範かつ詳細な解説

# あとがき

これでもう以下のプログラムみたいな悲惨なコードは書かなくても済みそうですね。

```cpp
char buffer[1000];
sprintf(buffer, "%d %d %d", a, b, d);
std::string str = buffer;
```

あとはVisual Studioで`printf`を使うとコンパイル時に書式をチェックしてくれる機能を`format`に対しても付けて欲しい。

さらに贅沢を言えば、メジャーなコンパイラ環境のどこでも使えるようになってくれたら……