株式会社ホコサキ

C++の文字列フォーマットをfmtで書き直す実践

天京祐輔
天京祐輔
C++の文字列フォーマットをfmtで書き直す実践

GitHub Copilot や ChatGPT が生成した C++ コードを読んでいると、fmt::format という関数が出てくることが増えました。
printf でも std::cout でもなく、波かっこのプレースホルダー。
「動くコードが出てきたけど、これは何?」という状態のまま使い続けているエンジニアは、正直少なくないと思います。

AI ツールの学習データには fmtlib を使った OSS コードが豊富に含まれているため、Copilot や ChatGPT が提案するコードに fmt が自然と登場します。
コードレビューで fmt::format が出てきて「見たことはあるけど詳しくは知らない」という場面は、これからも増えていくはずです。

ここでは、printf と ostringstream で書かれた既存コードが抱えている問題をコードで具体的に示し、{fmt} に書き直す方法を追います。
CMake や vcpkg 経由での導入手順、std::format(C++20)との使い分けも整理します。

printf と ostringstream の「じわじわ来る痛み」

正直なところ、printf も ostringstream も「動いているうちは問題に気づきにくい」のが厄介です。
型の問題が実行時まで見えないし、読みにくさも少しずつ積み重なる。
まずはその痛みを、具体的なコードで確認しておきます。

// printf: 型指定子のミスマッチ
double price = 1234.56;
printf("Price: %d\n", price);  // %d(整数用)に double を渡している

このコードは gcc/clang に -Wall を渡せば警告が出ますが、コンパイル自体は通ります。
出力される値は意図しないものになります。
「フォーマット指定子と引数の型を合わせる」という作業を、人間がずっと手作業で管理し続けなければならない。
それが printf の根本的な設計です。

引数が増えるほどリスクは上がります。
ログ出力にエラーコード・スレッド ID・ファイルパス・行番号を並べたいとき、6〜8 個の引数を printf に渡すコードを書いた経験があれば分かると思います。
指定子を一つ間違えたまま本番ログが化け続けた、という経験をした人もいるはずです。

ostringstream はその点では型安全ですが、今度は冗長さが問題になります。

// ostringstream: 書くのが面倒
std::ostringstream oss;
oss << "Error [" << error_code << "]: "
    << message << " (file: " << filename << ", line: " << line << ")";
std::string result = oss.str();

ローカル変数の宣言、演算子の連鎖、最後の str() 呼び出し。
一か所に書くだけなら我慢できますが、ログ・エラーメッセージ・帳票整形と、文字列フォーマットが頻出する業務コードでは積み重なります。
さらに、ostringstream は std::string を取り出すときにコピーが発生するため、パフォーマンスの観点でも素直には褒めにくいです。
「動いているから問題ない」という判断が、読みにくいコードを静かに増やしていく。
それがこの問題の厄介なところです。

fmt の基本構文と型安全性の仕組み

fmt の基本的な使い方は、printf の感覚に近いです。
ただし、フォーマット指定子の代わりに波かっこ {} でプレースホルダーを書き、型は自動的に推論されます。

先ほどの printf コードを fmt で書き直すとこうなります。

#include <fmt/core.h>

double price = 1234.56;
// 型を自動推論。指定子のミスマッチはそもそも起きない
std::string s = fmt::format("Price: {}\n", price);
fmt::print("Price: {}\n", price);  // 直接出力するなら fmt::print

フォーマット指定子は波かっこの中にコロンで区切って書きます。
業務でよく使うゼロ埋め・小数点桁数・16進数をまとめるとこうなります。

int id = 42;
double ratio = 0.9876;
int hex_code = 255;

// ゼロ埋め(幅5桁)
fmt::print("ID: {:05d}\n", id);        // → "ID: 00042"

// 小数点以下2桁
fmt::print("Ratio: {:.2f}\n", ratio);  // → "Ratio: 0.99"

// 16進数(大文字)
fmt::print("Code: {:X}\n", hex_code);  // → "Code: FF"

printfの書式指定と構造は似ていますが、コロンが % の代わりになっていると思えば読めます。
printf に慣れている人なら、大半の指定子は対応表なしで読み替えられると思います。

ここで 重要 なのは、型ミスがビルド時に検出されるという点です。
fmt はフォーマット文字列をコンパイル時に評価しており、型と指定子が合わない場合はコンパイルエラーになります。
printfのように「警告は出るけど通る」ではなく、ビルドが止まるので確実に気づけます。
エラーメッセージも比較的読みやすく、どの引数が問題なのかが示されます。
「実行してみたら出力が変だった」という後追いのデバッグが、ビルドフェーズに前倒しになる差は、積み重なると大きいです。

パフォーマンスについても触れておきます。
fmtlib 公式の dtoa-benchmark によれば、float/double のフォーマット処理では std::ostringstream や sprintf の 20〜30 倍速い という結果が示されています。
浮動小数点数の文字列変換はログや帳票整形で頻繁に発生するため、この差が効いてくる場面は多いです。
整数や文字列でも printf よりおおむね高速で、コンパイル時にフォーマット文字列が解析されることが効率につながっています。

業務コードの3場面で fmt を使う

fmt が具体的にどう役立つか、業務でよく出くわす3つの場面で見てみます。

ログ出力

ログにはスレッド ID・ファイル名・行番号・メッセージを並べることが多いです。
ostringstream で書いていたものが、fmt を使うとこうなります。

// Before: ostringstream
std::ostringstream oss;
oss << "[WARN] thread=" << thread_id
    << " file=" << filename << ":" << line
    << " msg=" << message;
logger.write(oss.str());

// After: fmt
logger.write(fmt::format(
    "[WARN] thread={} file={}:{} msg={}",
    thread_id, filename, line, message
));

引数の順序とプレースホルダーの対応が一目で分かります。
spdlog などの主要ロギングライブラリは fmt をバックエンドに使っているため、fmt の書き方を覚えると spdlog のフォーマット文字列もそのまま読めます。
「fmt を覚えると周辺ライブラリの読み書きにも直結する」というのは、地味に大きいメリットです。

エラーメッセージ生成

例外メッセージや API レスポンスの文字列生成には、可読性が特に重要です。

// Before: snprintf + char バッファ
char buf[256];
snprintf(buf, sizeof(buf), "Device 0x%04X not found in zone %d", device_id, zone);
throw std::runtime_error(buf);

// After: fmt
throw std::runtime_error(
    fmt::format("Device {:#06X} not found in zone {}", device_id, zone)
);

snprintf のバッファサイズ管理が不要になります。
{:#06X} は「0x プレフィックス付き・大文字・6桁ゼロ埋め」の 16 進数を意味します。
指定子の構文は慣れが必要ですが、printf の %04X に慣れていれば形式的な違いは小さいです。

数値・日付の帳票整形

帳票や CSV 出力では、桁数・揃え・小数点桁数を統一したい場面が多いです。

// 金額: 小数点以下2桁・右揃え10文字
fmt::print("{:>10.2f}\n", 12345.6);   // → "  12345.60"

// 日付(tm 構造体をそのまま渡せる。fmt/chrono.h が必要)
std::tm t = get_current_time();
fmt::print("{:%Y-%m-%d %H:%M:%S}\n", t);  // → "2025-07-14 10:30:00"

日付フォーマットは fmt/chrono.h をインクルードすれば std::tm や std::chrono の型をそのまま渡せます。
strftime でバッファを用意していた処理が1行に収まります。
時刻フォーマットのトークンは strftime と同じなので、移行コストも低いです。

std::format(C++20)との関係と、どちらを選ぶか

std::format は C++20 で標準ライブラリに追加された文字列フォーマット機能です。
その 設計の原型は fmtlib であり、提案者は fmtlib の作者である Victor Zverovich 氏本人です。
API の形は意図的に合わせてあるため、fmt::format を覚えれば std::format もほぼそのまま読めます。
AI が生成するコードで std::format と fmt::format が混在していても、フォーマット文字列の書き方はほぼ同じなので混乱する必要はありません。

どちらを選ぶべきか、主な差分を整理しておきます。

  • C++17 以前の環境では std::format は使えないため、fmt 一択です。
  • fmt::print は C++20 標準に含まれていません(直接出力に対応した std::print は C++23 で追加)。
  • std::format では C++20 の時点でターミナルへの直接出力に便利な関数がなく、迂回した書き方が必要になります。
  • 日付・時刻や Unicode 周りは、標準より fmt の対応が先行している部分があります。

C++20 移行済みの環境でも、fmt::print が使いたい・最新機能が必要という場面では fmt を選ぶ理由は十分あります。
「std::format で足りるなら標準を使う、足りなければ fmt を使う」という判断が現実的です。
いずれにしても、fmtlib の書き方を覚えておくことが std::format の理解にも直結するため、無駄にはなりません。

既存コードベースへの段階的な導入手順

fmt を既存プロジェクトに追加する方法は主に2つです。
CMake の FetchContent を使う方法と、vcpkg 経由で find_package を使う方法です。

FetchContent を使う場合、CMakeLists.txt にこう書きます。

include(FetchContent)
FetchContent_Declare(
  fmt
  GIT_REPOSITORY https://github.com/fmtlib/fmt.git
  GIT_TAG        11.1.0  # 使用するバージョンに合わせる
)
FetchContent_MakeAvailable(fmt)

target_link_libraries(your_target PRIVATE fmt::fmt)

外部ツールなしにリポジトリを取得できるため、CI 環境や初めて導入するプロジェクトには手軽です。
vcpkg や Conan を別途管理したくない場合に向いています。

vcpkg を既に使っている場合はこちらの方がバージョン管理が楽です。
まずターミナルでこのコマンドを実行します。

vcpkg install fmt

インストール後、CMakeLists.txt にはこう書きます。

find_package(fmt CONFIG REQUIRED)
target_link_libraries(your_target PRIVATE fmt::fmt)

導入したら、次は どこから書き換えるか という判断です。
一気に全部置き換える必要はなく、リスクとリターンが大きい箇所から手を付けるのが現実的です。

優先度が高いのは、引数が3個以上ある printf 呼び出しです。
型指定子のミスマッチが潜みやすく、書き換えることで安全性が上がります。
次に優先したいのは、ostringstream を使った複雑なエラーメッセージやログ生成の処理です。
可読性が大きく改善されるため、後から読む人(未来の自分含む)へのコストが下がります。

clang-tidy v18 には modernize-use-std-print というチェックがあり、printf や fprintf を fmt::print(または std::print)へ自動変換する機能があります。
fmtlib 公式の README にも記載されているため、半自動移行の足がかりとして活用できます。

完全な移行を目指すより「新しく書く箇所は fmt、既存は優先度をつけて段階的に」というスタンスの方が、実際のプロジェクトでは続きます。
コードベース全体が少しずつ読みやすくなっていく変化は、地味ですが確実に効いてきます。


株式会社ホコサキは、山口県宇部を拠点に業務システム開発・AI 活用支援・DX 推進を手がけています。
C++ を使った業務コードの品質改善や段階的なモダン化など、実装レベルの相談にも対応しています。
ご興味があれば お問い合わせページ からお気軽にどうぞ。

    C++の文字列フォーマットをfmtで書き直す実践 | 株式会社ホコサキ