C++20のstd::formatがコンパイル時フォーマット検査を実現する仕組み

C++20に導入されたstd::formatは非常に強力で実用的な機能を提供します。その最大の特徴の一つは、特別な記述を必要とせず、通常の関数呼び出しと同じように使用するだけで、文字列のフォーマットが正しいかどうかをコンパイル時に検査できる点です。

#include <format>

int value = 42;
std::string result1 = std::format("値: {}", value); // 正常
std::string result2 = std::format("値: {}, 説明: {}", value); // コンパイルエラー

このstd::formatは、有名なオープンソースライブラリ{fmt}をベースにしています。C++20が登場する前のバージョンでは、{fmt}ライブラリはコンパイル時フォーマット検査を実現するために、各文字列リテラルに対して異なる型を作成する必要がありました。そのため、ユーザー向けにFMT_STRINGマクロが提供され、使用を簡略化していました。

#include <fmt/format.h>

int value = 42;
std::string result1 = fmt::format(FMT_STRING("値: {}"), value); // 正常
std::string result2 = fmt::format(FMT_STRING("値: {}, 説明: {}"), value); // コンパイルエラー

C++20でconstevalが導入されたことで、このような煩雑な記述は不要になりました。constexpr関数と異なり、consteval関数は必ずコンパイル時に評価されます。std::formatはこのconstevalを利用して、文字列引数のフォーマットをコンパイル時に検査しています。

しかし、std::format自体をconsteval関数にすることはできません。そこで、補助型であるstd::format_stringを導入し、文字列実引数をstd::format_string型に暗黙的に変換させる手法が取られています。この変換関数がconstevalで実装されており、フォーマット検査のロジックが含まれていることで、コンパイル時のフォーマット検査が実現されます。

以下に、プレースホルダ{}の数が引数の数と一致するかを検査する簡略版のformat実装を示します。format_stringのコンストラクタが求める暗黙的変換関数であり、constevalで宣言されています。フォーマット文字列内の{}の数が引数の数と一致しない場合、throw文が実行されます。C++ではthrow文はコンパイル時に評価できないため、コンパイルエラーが発生し、フォーマットエラーが検出されます。

namespace my {
    template<typename... Args>
    class format_string {
    private:
        std::string_view format_str;
        
    public:
        template<typename T>
            requires std::convertible_to<const T&, std::string_view>
        consteval format_string(const T& s)
            : format_str(s)
        {
            std::size_t placeholder_count = 0;
            for (std::size_t i = 0; i + 1 < format_str.length(); ++i) {
                if (format_str[i] == '{' && format_str[i + 1] == '}') {
                    ++placeholder_count;
                }
            }
            
            constexpr std::size_t arg_count = sizeof...(Args);
            if (placeholder_count != arg_count) {
                throw std::format_error("フォーマット文字列が不正です");
            }
        }
        
        std::string_view get() const { return format_str; }
    };
    
    template<typename... Args>
    std::string format(format_string<std::type_identity_t<Args>...> fmt, Args&&... args) {
        // フォーマットの具体的な実装は省略
    }
}

ここで重要なポイントは、format関数のパラメータがformat_string<std::type_identity_t<Args>...>と記述されている点です。直接format_string<Args...>と記述すると、暗黙的変換が機能しません。

これは、テンプレート実引数推論が暗黙的変換を考慮しないためです。C++20ではこの問題を解決するためにstd::type_identityが導入されています。std::type_identityは単純に型をそのまま返すだけのツールですが、これを利用することでテンプレート実引数推論において非推論コンテキスト(non-deduced context)を確立し、暗黙的変換を正常にマッチさせることが可能になります。C++言語のこうした仕組みは時に奇妙に見えることがありますが、こうしたテクニックによって強力な機能を実現しています。

タグ: C++20 std::format consteval コンパイル時チェック テンプレートメタプログラミング

7月7日 19:14 投稿