はじめに
C++20で導入された**std::format
**は、printf
のように、型安全で拡張性の高い書式設定機能を提供します。int
やstring
といった基本型は標準でサポートされていますが、「自作したクラスやenum(列挙型)」をformat
で直接扱いたい、という場面が必ず出てきます。
これを実現するには、std::formatter
というクラステンプレートを、自作の型のために特殊化(カスタマイズ)する必要があります。formatter
を特殊化することで、std::format
に対して、自作の型をどのように文字列に変換すればよいかを教えることができます。
この記事では、formatter
を特殊化するための2つの主要なパターンを解説します。
- シンプルな特殊化: 書式指定なしで、基本的な文字列変換を定義する。
- 書式指定をパースする特殊化:
{:%j}
のようなカスタム書式指定を解釈し、出力を切り替える。
【前提】C++20とは?
C++20は、2020年に正式化されたC++言語のメジャーアップデート版です。<format>
ライブラリはこのC++20で導入されたため、利用するにはC++20に対応したコンパイラが必要です。
1. シンプルなformatter
特殊化
ここでは、Signal
というenum
型を、対応する文字列(”Red”, “Yellow”, “Green”)に変換するformatter
を作成します。
完成コード
#include <iostream>
#include <format> // format, formatter
#include <string>
using namespace std;
// 書式設定をカスタマイズしたい自作のenum
enum class Signal { Red, Yellow, Green };
// 対応する文字列の配列
const char* signal_names[] = { "Red", "Yellow", "Green" };
// 1. formatter<Signal> の特殊化
template<>
struct std::formatter<Signal> : std::formatter<const char*> {
// 2. format関数を定義
auto format(Signal s, format_context& ctx) const {
// 内部で const char* のformatterを呼び出す
return formatter<const char*>::format(signal_names[static_cast<int>(s)], ctx);
}
};
int main() {
cout << format("現在の信号: {}", Signal::Green) << endl;
return 0;
}
解説:
template<> struct std::formatter<Signal> : ...
:std::formatter
を、自作の型Signal
のために特殊化しています。std::formatter<const char*>
から継承するのが簡単な方法です。auto format(...)
:format
メンバ関数を定義します。この中で、Signal
のenum値(0
,1
,2
…)をインデックスとして使い、対応するC言語スタイルの文字列 (const char*
) を配列から取得します。そして、親クラスであるformatter<const char*>
のformat
関数を呼び出して、実際の書式設定処理を委譲しています。
2. カスタム書式指定をパースするformatter
特殊化
次に、"{:%J}"
(日本語)や "{:%E}"
(英語)のような、独自の書式指定を解釈できる、より高度なformatter
を作成します。
完成コード
#include <iostream>
#include <format>
#include <string>
using namespace std;
enum class Signal { Red, Yellow, Green };
// 英語名と日本語名の配列
const char* en_signal_names[] = { "Red", "Yellow", "Green" };
const char* jp_signal_names[] = { "赤", "黄", "緑" };
// formatter<Signal> の特殊化
template<>
struct std::formatter<Signal> {
private:
bool use_japanese = false;
public:
// 1. parse関数で書式指定子を解釈
constexpr auto parse(format_parse_context& ctx) {
auto it = ctx.begin();
if (it != ctx.end() && *it == 'J') {
use_japanese = true;
++it;
}
return it;
}
// 2. format関数で、解釈した結果に応じて出力を切り替え
auto format(Signal s, format_context& ctx) const {
const char* name = use_japanese ? jp_signal_names[static_cast<int>(s)]
: en_signal_names[static_cast<int>(s)];
return format_to(ctx.out(), "{}", name);
}
};
int main() {
cout << format("信号 (英語): {:}", Signal::Red) << endl;
cout << format("信号 (日本語): {:%J}", Signal::Green) << endl;
return 0;
}
解説:
constexpr auto parse(...)
:parse
メンバ関数を実装すると、std::format
は書式指定文字列({}
の中の:
以降の部分)をこの関数に渡します。ctx.begin()
でイテレータを取得し、*it
で文字を調べることで、'J'
のようなカスタム指定子を解釈し、メンバ変数use_japanese
に状態を保存します。auto format(...)
:format
関数では、parse
で設定されたuse_japanese
の値を見て、日本語と英語のどちらの配列を使うかを切り替えています。
まとめ
今回は、C++20の<format>
ライブラリで、std::formatter
を特殊化して自作の型を文字列化する方法を解説しました。
- シンプルな変換:
std::formatter
の既存の特殊化(例:string
やconst char*
)から継承し、format
関数を定義するのが簡単。 - カスタム書式: **
parse
メンバ関数を実装して独自の書式指定子を解釈し、format
**メンバ関数でその結果に応じた出力をする。
formatter
を特殊化することで、自作のクラスやenumを、int
などの組み込み型と全く同じように、std::format
で直感的かつ安全に扱えるようになります。