ついに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
に対しても付けて欲しい。
さらに贅沢を言えば、メジャーなコンパイラ環境のどこでも使えるようになってくれたら……