【C++20】std::formatで自作の型を文字列化する方法 (formatterの特殊化)

目次

はじめに

C++20で導入された**std::format**は、printfのように、型安全で拡張性の高い書式設定機能を提供します。intstringといった基本型は標準でサポートされていますが、「自作したクラスやenum(列挙型)」をformatで直接扱いたい、という場面が必ず出てきます。

これを実現するには、std::formatter というクラステンプレートを、自作の型のために特殊化(カスタマイズ)する必要があります。formatterを特殊化することで、std::format に対して、自作の型をどのように文字列に変換すればよいかを教えることができます。

この記事では、formatterを特殊化するための2つの主要なパターンを解説します。

  1. シンプルな特殊化: 書式指定なしで、基本的な文字列変換を定義する。
  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;
}

解説:

  1. template<> struct std::formatter<Signal> : ...: std::formatter を、自作の型 Signal のために特殊化しています。std::formatter<const char*> から継承するのが簡単な方法です。
  2. 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;
}

解説:

  1. constexpr auto parse(...): parseメンバ関数を実装すると、std::format は書式指定文字列({}の中の:以降の部分)をこの関数に渡します。ctx.begin() でイテレータを取得し、*it で文字を調べることで、'J' のようなカスタム指定子を解釈し、メンバ変数 use_japanese に状態を保存します。
  2. auto format(...): format関数では、parseで設定された use_japanese の値を見て、日本語と英語のどちらの配列を使うかを切り替えています。

まとめ

今回は、C++20の<format>ライブラリで、std::formatterを特殊化して自作の型を文字列化する方法を解説しました。

  • シンプルな変換: std::formatterの既存の特殊化(例: stringconst char*)から継承し、format関数を定義するのが簡単。
  • カスタム書式: **parseメンバ関数を実装して独自の書式指定子を解釈し、format**メンバ関数でその結果に応じた出力をする。

formatterを特殊化することで、自作のクラスやenumを、intなどの組み込み型と全く同じように、std::formatで直感的かつ安全に扱えるようになります。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次