ついに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のようなライブラリも登場しましたが、やはり標準ライブラリに入っていないと導入が大変でした(最近ようやくC++界隈でもパッケージマネージャが広まってきましたが)。
そんなわけで、C++20にて新しい書式つき文字列のためのライブラリformatが導入されると聞き、私のように期待していたC++開発者も多いのではないでしょうか。
そして、2021年5月末、ようやくMicrosoftがVisual Studio 2019(16.10)にてformatの実装をリリースしました。GNU(libstdc++)やClang(libcxx)より先に出してくるとは思っていませんでした。近年のMicrosoftは本当にすごい。
Visual Studio 2019 バージョン16.10 のリリースノート
libstdc++のC++20開発状況 -> P0645をページ内検索
libcxxのC++20開発状況 -> P0645をページ内検索
導入
特に難しいことは何もないので、詳しくは解説しません。
-
Visual Studio 2019のアップデート
- Visual Studio 2019を持っていない方
- Visual Studio 2019をインストールします(どのEditionでもOK)
- Visual Studio 2019を持っている方
-
Visual Studio Installerを実行し、2019を最新にアップデートします。 - その後
Visual Studio 2019を起動し、バージョンが16.10.0以降になっていればOK
-
- Visual Studio 2019を持っていない方
-
C++のプロジェクトを作成
- プロジェクトの種類はなんでもいいです(私は「コンソールアプリ」で試しています)
-
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をインクルードするだけで使用できます。
# 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やPythonのf-strings、Rustのformatに近しいもので、これらを使ったことがある人ならすぐに使いこなすことができるでしょう。
書式文字列の中に変数を差し込みたい場合、カーリーブラケット{}を使用してその場所を指定します。
詳しい使い方についてはすでにQiitaに記事があるので、そちらを参照してください。
C++20の文字列フォーマットライブラリ std::format
例外
以下では上記の記事では触れていないポイントについて簡単に解説していきます。
std::formatでは書式や引数が不正な場合、実行時例外としてstd::format_errorを発生させます。
try {
// 1個しか引数がないのに、2個目を使用しようとしてみる
cout << format("{1}", 0) << endl;
}
catch (const format_error& e)
{
cout << e.what() << endl;
}
↓
Argument not found.
書式文字列が不正な場合も例外が発生します。
format("{$324i0-g}", 0)
Invalid format string.
書式内で指定した型と引数の型が違う場合も例外が発生します。
format("{:d}", 4.0f)
invalid floating point type
format("{:f}", 42)
invalid integral type
fill指定に{や}を使うとエラーになります(これ以外の文字は使える)。
format("{:{>10}", 42)
invalid fill character '{'
format("{:}>10}", 42)
Unmatched '}' in format string.
浮動小数点数以外でprecision指定はできません。
format("{:6.3}", 42)
Precision not allowed for this argument type.
ワイド文字列
もちろんwchar_tやwstringに対応しているので、日本語も扱えます。
# include <iostream>
# include <format>
# include <locale>
int main()
{
using namespace std;
setlocale(LC_CTYPE, "");
wcout << format(L"{}", L"こんにちは!") << endl;
}
こんにちは!
fill指定にもワイド文字は使えますが、期待した動作にはならないようです。
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 |
| 最適な表記を自動判定 (gは省略可) |
format("{:g}", 123.456789) | 123.457 |
| 左寄せ | format("{:<12}", "left") | left |
| 右寄せ | format("{:>12}", "right") | right |
| 中央寄せ | format("{:^12}", "center") | center |
| 埋める文字を指定する | format("{:!^12}", "hello") | !!!hello!!!! |
参考文献
Text Formatting - www.open-std.org: C++標準化委員会による仕様
format - cpprefjp - C++日本語リファレンス: 広範かつ詳細な解説
あとがき
これでもう以下のプログラムみたいな悲惨なコードは書かなくても済みそうですね。
char buffer[1000];
sprintf(buffer, "%d %d %d", a, b, d);
std::string str = buffer;
あとはVisual Studioでprintfを使うとコンパイル時に書式をチェックしてくれる機能をformatに対しても付けて欲しい。
さらに贅沢を言えば、メジャーなコンパイラ環境のどこでも使えるようになってくれたら……