C++之fmt庫介紹與使用(1)
Author: Once Day Date: 2025年5月12日
一位熱衷于Linux學習和開發的菜鳥,試圖譜寫一場冒險之旅,也許終點只是一場白日夢…
漫漫長路,有人對你微笑過嘛…
全系列文章可參考專欄: 源碼分析_Once-Day的博客-CSDN博客
參考文章:
- Get Started - {fmt}
- fmtlib/fmt: A modern formatting library
文章目錄
- C++之fmt庫介紹與使用(1)
- 1. 介紹
- 1.1 概述
- 1.2 性能對比
- 1.3 編譯時間和代碼膨脹
- 1.4 CMake編譯
- 2. API介紹
- 2.1 基礎API
- 2.2 格式化用戶定義類型
- 2.3 編譯時檢測
- 2.4 命名參數(Named Arguments)
- 2.5 類型擦除
- 2.6 兼容性
1. 介紹
1.1 概述
fmtlib是一個現代化的C++格式化庫,它提供了一種安全、高效、靈活的方式來格式化和輸出文本。該庫由Victor Zverovich開發,旨在替代C++標準庫中的iostream和printf等傳統格式化方法。
使用fmtlib非常簡單,只需包含fmt/core.h
頭文件,然后使用fmt::format()
函數或fmt::print()
函數即可。例如:
#include <fmt/core.h>int main() {std::string name = "Alice";int age = 18;fmt::print("Her name is {} and she is {} years old.\n", name, age);return 0;
}
以上代碼將輸出:“Her name is Alice and she is 18 years old.”
fmtlib具有具有如下的特性:
(1)安全性:受 Python 格式化功能的啟發,{fmt} 為printf
系列函數提供了安全的替代方案。格式字符串中的錯誤在 C 語言中是常見的漏洞來源,而在 {fmt} 中,這類錯誤會在編譯時被報告出來。
fmt::format("{:d}", "I am not a number");
上述代碼會產生編譯時錯誤,因為d
不是字符串的有效格式說明符。像fmt::format
這樣的 API 通過自動內存管理來防止緩沖區溢出錯誤。
(2)可擴展性:{fmt} 開箱即支持大多數標準類型的格式化,包括所有容器、日期和時間。例如:
fmt::print("{}", std::vector{1, 2, 3});
上述代碼會以類似 JSON 的格式打印向量:
[1, 2, 3]
你可以讓自己定義的類型支持格式化,甚至還能對它們進行編譯時檢查。
(3)性能:{fmt} 比輸入輸出流(iostreams)和sprintf
快 20 - 30 倍,在數值格式化方面表現尤為突出。
{fmt} 庫盡量減少動態內存分配,并且可以選擇將格式字符串編譯為最優代碼。
(4)Unicode 支持:{fmt} 在主要操作系統上通過 UTF - 8 和char
字符串提供可移植的 Unicode 支持。例如:
fmt::print("Слава Укра?н?!");
上述代碼在 Linux、macOS 甚至 Windows 控制臺上都能正確打印,而無需考慮代碼頁問題。
{fmt} 默認與區域設置無關,但你也可以選擇進行本地化格式化,{fmt} 能夠使其與 Unicode 協同工作,解決了標準庫中存在的相關問題。
(5)快速編譯:該庫廣泛使用類型擦除技術來實現快速編譯。fmt/base.h
提供了一部分 API,其包含的依賴關系極少,并且具備足夠的功能來替代所有*printf
的使用場景。
使用 {fmt} 的代碼編譯速度通常比等效的輸入輸出流代碼快幾倍。雖然printf
的編譯速度仍然更快,但兩者之間的差距正在逐漸縮小。
(6)較小的二進制體積:類型擦除技術還用于防止模板膨脹,從而生成緊湊的單次調用二進制代碼。例如,調用帶單個參數的fmt::print
僅需幾條指令,盡管它增加了運行時安全性,但其二進制體積與printf
相當,并且比等效的輸入輸出流代碼小得多。
該庫本身的二進制體積較小,像浮點格式化這樣的一些組件可以被禁用,以便在資源受限的設備上進一步減小其體積。
(7)可移植性:{fmt} 擁有一個小巧且自包含的代碼庫,其核心僅由三個頭文件組成,并且沒有外部依賴。
該庫具有高度的可移植性,僅需要 C++11 的一小部分特性,這些特性在 GCC 4.9、Clang 3.4、MSVC 19.10(2017)及更高版本中均可用。如果編譯器和標準庫支持更新的特性,{fmt} 會加以利用,從而啟用更多功能。
在可能的情況下,格式化函數的輸出在各個平臺上保持一致。
在可能的情況下,格式化函數的輸出在各個平臺上保持一致。
(8)開源:{fmt} 是 GitHub 上排名前一百的開源 C++ 庫,擁有數百名貢獻者。
該庫基于寬松的 MIT 許可證分發,被許多開源項目所依賴,包括 Blender、PyTorch、蘋果的 FoundationDB、Windows Terminal、MongoDB 等。
fmtlib的主要特點包括:
- 簡單的 格式化 API,支持位置參數以方便本地化。
- 實現了 C++20 的
std::format
和 C++23 的std::print
。 - 格式化字符串語法 類似于 Python 的 format。
- 快速的 IEEE 754 浮點數格式化器,使用 Dragonbox 算法,保證正確的舍入、最短表示和往返轉換。
- 可移植的 Unicode 支持。
- 安全的 printf 實現,包括支持位置參數的 POSIX 擴展。
- 可擴展性:支持用戶自定義類型。
- 高性能:比常見的標準庫實現(如
(s)printf
、iostreams、to_string
)更快。 - 安全性:該庫是完全類型安全的,格式化字符串中的錯誤可以在編譯時報告,自動內存管理可防止緩沖區溢出錯誤。
- 易用性:代碼庫小巧且自包含,無外部依賴,采用寬松的 MIT 許可證。
- 可移植性:跨平臺輸出一致,支持較舊的編譯器。
- 代碼干凈,即使在高警告級別(如
-Wall -Wextra -pedantic
)下也無警告。 - 默認與區域設置無關。
- 可通過
FMT_HEADER_ONLY
宏啟用可選的僅頭文件配置。
1.2 性能對比
{fmt}是基準測試中最快的方法,比printf快約20%。
Library | Method | Run Time, s |
---|---|---|
libc | printf | 0.91 |
libc++ | std::ostream | 2.49 |
{fmt} 9.1 | fmt::print | 0.74 |
Boost Format 1.80 | boost::format | 6.26 |
Folly Format | folly::format | 1.87 |
上述結果是在macOS 12.6.1上使用clang++ -O3 -DNDEBUG -DSPEED_TEST -DHAVE_FORMAT編譯tinyformat_test.cpp,并取三次運行中的最佳結果生成的。
在測試中,格式字符串%0.10f:%04d:%+g:%s:%p:%c:%%\n
或等效字符串被填充2,000,000次,輸出被發送到/dev/null
;
在IEEE754浮點數和雙精度格式化(dtoa-benchmark)方面,{fmt}比std::ostringstream和sprintf快20-30倍,并且比double-conversion和ryu更快:
1.3 編譯時間和代碼膨脹
format-benchmark中的腳本bloat-test.py測試了非平凡項目的編譯時間和代碼膨脹。它生成100個翻譯單元,并在每個單元中使用printf()或其替代方法五次,以模擬一個中等規模的項目。生成的可執行文件大小和編譯時間(Apple clang version 15.0.0 (clang-1500.1.0.2.5),macOS Sonoma,三次中的最佳結果)如下表所示。
優化構建(-O3):
Method | Compile Time, s | Executable size, KiB | Stripped size, KiB |
---|---|---|---|
printf | 1.6 | 54 | 50 |
IOStreams | 25.9 | 98 | 84 |
fmt 83652df | 4.8 | 54 | 50 |
tinyformat | 29.1 | 161 | 136 |
Boost Format | 55.0 | 530 | 317 |
{fmt}編譯速度快,在每次調用的二進制大小方面與printf相當(在此系統上在舍入誤差范圍內)。
非優化構建:
Method | Compile Time, s | Executable size, KiB | Stripped size, KiB |
---|---|---|---|
printf | 1.4 | 54 | 50 |
IOStreams | 23.4 | 92 | 68 |
{fmt} 83652df | 4.4 | 89 | 85 |
tinyformat | 24.5 | 204 | 161 |
Boost Format | 36.4 | 831 | 462 |
libc、lib(std)c++和libfmt都作為共享庫進行鏈接,以僅比較格式化函數的開銷。Boost Format是一個僅包含頭文件的庫,因此它不提供任何鏈接選項。
1.4 CMake編譯
{fmt} 提供了兩個 CMake 目標:fmt::fmt
用于編譯庫,fmt::fmt-header-only
用于僅包含頭文件的庫。為了縮短構建時間,建議使用編譯庫。
在 CMake 中使用 {fmt} 主要有三種方式:
FetchContent:從 CMake 3.11 開始,可以使用FetchContent
在配置時自動下載 {fmt} 作為依賴項:
include(FetchContent)FetchContent_Declare(fmtGIT_REPOSITORY https://github.com/fmtlib/fmtGIT_TAG e69e5f977d458f2650bb346dadf2ad30c5320281) # 10.2.1FetchContent_MakeAvailable(fmt)target_link_libraries(<your-target> fmt::fmt)
已安裝版本:可以在CMakeLists.txt
文件中查找并使用已安裝的 {fmt} 版本,如下所示:
find_package(fmt)
target_link_libraries(<your-target> fmt::fmt)
嵌入方式:可以將 {fmt} 的源文件目錄添加到項目中,并在CMakeLists.txt
文件中包含它:
add_subdirectory(fmt)
target_link_libraries(<your-target> fmt::fmt)
安裝發布版本:要在 Ubuntu 的 Linux 發行版上安裝 {fmt},請使用以下命令:
apt install libfmt-dev
從源代碼構建:CMake 通過生成原生的 makefile 或項目文件來工作,這些文件可以在你選擇的編譯器環境中使用。典型的工作流程如下:
mkdir build # 創建一個目錄來存放構建輸出。
cd build
cmake .. # 生成原生構建腳本。
常見的Cmake編譯構建選項如下所示:
# 編譯 thirdparty/fmt-11.1.4
# FMT_MASTER_PROJECT=OFF 非主項目
# FMT_UNICODE=OFF 不支持Unicode
$SOURCE_DIR/devops/scripts/cmake_build.sh thirdparty/fmt-11.1.4 \-DFMT_MASTER_PROJECT=OFF \-DFMT_UNICODE=OFF
2. API介紹
{fmt} 庫的 API 由以下組件構成:
- fmt/base.h:基礎 API,提供面向 char/UTF-8 的主要格式化函數,具備 C++20 編譯時檢查功能,且依賴極少。
- fmt/format.h:包含 fmt::format 及其他格式化函數,同時提供本地化支持。
- fmt/ranges.h:用于格式化范圍(ranges)和元組(tuples)。
- fmt/chrono.h:實現日期和時間的格式化。
- fmt/std.h:為標準庫類型提供格式化器。
- fmt/compile.h:用于格式化字符串編譯。
- fmt/color.h:提供終端顏色和文本樣式功能。
- fmt/os.h:包含系統相關 API。
- fmt/ostream.h:提供對 std::ostream 的支持。
- fmt/args.h:支持動態參數列表。
- fmt/printf.h:提供安全的 printf 功能。
- fmt/xchar.h:提供可選的 wchar_t 支持。
該庫提供的所有函數和類型都位于 fmt 命名空間中,而宏則以 FMT_ 為前綴。
2.1 基礎API
fmt/base.h
定義了基礎 API,它為 char
/UTF-8 提供主要的格式化函數,并具備 C++20 編譯時檢查功能。為了優化編譯速度,它的頭文件依賴被減至最少。這個頭文件僅在將 {fmt} 作為庫使用時(默認方式)才有優勢,在僅頭文件模式下并無作用。它還為以下類型提供了格式化器特化:
int
,long long
unsigned
,unsigned long long
float
,double
,long double
bool
char
const char*
,fmt::string_view
const void*
以下函數使用的格式字符串語法類似于 Python 中 str.format
的語法。它們接受 fmt
和 args
作為參數:
fmt
是一個格式字符串,包含普通文本和用花括號{}
包圍的替換字段。這些字段會在結果字符串中被格式化為對應的參數。fmt::format_string
是一種格式字符串,它可以從字符串字面量或constexpr
字符串隱式構造,并在 C++20 中進行編譯時檢查。若要傳遞運行時格式字符串,需將其包裝在fmt::runtime
中。args
是一個參數列表,表示要格式化的對象。
除非另有說明,I/O 錯誤會以 std::system_error
異常的形式報告。
template <typename... T>
void print(format_string<T...> fmt, T&&... args);fmt::print("The answer is {}.", 42);
根據 fmt
中的規范格式化 args
,并將輸出寫入標準輸出(stdout
)。
template <typename... T>
void print(FILE* f, format_string<T...> fmt, T&&... args);fmt::print(stderr, "Don't {}!", "panic");
根據 fmt
中的規范格式化 args
,并將輸出寫入文件 f
。
template <typename... T>
void println(format_string<T...> fmt, T&&... args);
根據 fmt
中的規范格式化 args
,將輸出寫入標準輸出(stdout
),并在末尾添加一個換行符。
template <typename... T>
void println(FILE* f, format_string<T...> fmt, T&&... args);
根據 fmt
中的規范格式化 args
,將輸出寫入文件 f
,并在末尾添加一個換行符。
template <typename OutputIt, typename... T>
auto format_to(OutputIt&& out, format_string<T...> fmt, T&&... args) -> remove_cvref_t<OutputIt>;auto out = std::vector<char>();
fmt::format_to(std::back_inserter(out), "{}", 42);
根據 fmt
中的規范格式化 args
,將結果寫入輸出迭代器 out
,并返回指向輸出范圍末尾之后的迭代器。format_to
不會追加終止空字符。
template <typename OutputIt, typename... T>
auto format_to_n(OutputIt out, size_t n, format_string<T...> fmt, T&&... args) -> format_to_n_result<OutputIt>;
根據 fmt
中的規范格式化 args
,將結果的最多 n
個字符寫入輸出迭代器 out
,并返回總輸出大小(未截斷)和指向輸出范圍末尾之后的迭代器。format_to_n
不會追加終止空字符。
template <typename OutputIt>
struct format_to_n_result;
OutputIt out;
:指向輸出范圍末尾之后的迭代器。size_t size;
:總輸出大小(未截斷)。
template <typename... T>
auto formatted_size(format_string<T...> fmt, T&&... args) -> size_t;
返回 format(fmt, args...)
輸出的字符數。
2.2 格式化用戶定義類型
{fmt} 庫為許多標準 C++ 類型提供了格式化器。有關范圍(ranges)和元組(包括 std::vector
等標準容器)的格式化器,請參閱 fmt/ranges.h
;有關日期和時間的格式化器,請參閱 fmt/chrono.h
;有關其他標準庫類型的格式化器,請參閱 fmt/std.h
。
有兩種方法可以使自定義類型支持格式化:提供 format_as
函數或特化 formatter
結構體模板。
如果你希望將自定義類型按照另一種具有相同格式說明符的類型進行格式化,可以使用 format_as
方法。該函數應接受你的類型對象,并返回一個可格式化類型的對象。它應與你的類型定義在同一命名空間中。
#include <fmt/format.h>namespace kevin_namespacy {enum class film {house_of_cards, american_beauty, se7en = 7
};auto format_as(film f) { return fmt::underlying(f); }}int main() {fmt::print("{}\n", kevin_namespacy::film::se7en); // 輸出: 7
}
下面這種方法更復雜,但能完全控制解析和格式化過程。要使用此方法,需為你的類型特化 formatter
結構體模板,并實現 parse
和 format
方法。
推薦的定義格式化器的方法是通過繼承或組合復用現有的格式化器。這樣可以支持標準格式說明符而無需自己實現。例如:
// color.h:
#include <fmt/base.h>enum class color {red, green, blue};template <> struct fmt::formatter<color>: formatter<string_view> {// parse 方法繼承自 formatter<string_view>。auto format(color c, format_context& ctx) const-> format_context::iterator;
};// color.cc:
#include "color.h"
#include <fmt/format.h>auto fmt::formatter<color>::format(color c, format_context& ctx) const-> format_context::iterator {string_view name = "unknown";switch (c) {case color::red: name = "red"; break;case color::green: name = "green"; break;case color::blue: name = "blue"; break;}return formatter<string_view>::format(name, ctx);
}
注意,formatter<string_view>::format
定義在 fmt/format.h
中,因此必須在源文件中包含該頭文件。由于 parse
方法繼承自 formatter<string_view>
,它將識別所有字符串格式規范,例如:
fmt::format("{:>10}", color::blue)
將返回 " blue"
。
一般來說,格式化器具有以下形式:
template <> struct fmt::formatter<T> {// 解析格式說明符并將其存儲在格式化器中。//// [ctx.begin(), ctx.end()) 是一個可能為空的字符范圍,// 包含從要解析的格式規范開始的格式字符串的一部分,例如在//// fmt::format("{:f} continued", ...);//// 該范圍將包含 "f} continued"。格式化器應解析說明符直到 '}' 或范圍結束。// 在這個例子中,格式化器應解析 'f' 說明符并返回指向 '}' 的迭代器。constexpr auto parse(format_parse_context& ctx)-> format_parse_context::iterator;// 使用存儲在格式化器中的已解析格式規范格式化 value,// 并將輸出寫入 ctx.out()。auto format(const T& value, format_context& ctx) const-> format_context::iterator;
};
建議至少支持適用于整個對象的填充(fill)、對齊(align)和寬度(width)選項,它們的語義應與標準格式化器中的相同。
你還可以為類層次結構編寫格式化器:
// demo.h:
#include <type_traits>
#include <fmt/core.h>struct A {virtual ~A() {}virtual std::string name() const { return "A"; }
};struct B : A {virtual std::string name() const { return "B"; }
};template <typename T>
struct fmt::formatter<T, std::enable_if_t<std::is_base_of_v<A, T>, char>> :fmt::formatter<std::string> {auto format(const A& a, format_context& ctx) const {return formatter<std::string>::format(a.name(), ctx);}
};// demo.cc:
#include "demo.h"
#include <fmt/format.h>int main() {B b;A& a = b;fmt::print("{}", a); // 輸出: B
}
注意:不允許同時提供格式化器特化和 format_as
重載。
上下文類型定義:
template <typename Char>
using basic_format_parse_context = parse_context<Char>;class context;context(iterator out, format_args args, detail::locale_ref loc);
構造一個上下文對象。對象中存儲了對參數的引用,因此請確保這些參數具有適當的生命周期。
using format_context = context;
2.3 編譯時檢測
在支持 C++20 consteval
的編譯器上,編譯時格式字符串檢查默認是啟用的。在較舊的編譯器上,你可以使用fmt/format.h
中定義的FMT_STRING
宏來替代。
和 Python 的str.format
以及普通函數一樣,{fmt} 允許存在未使用的參數。
template <typename Char, typename... T>
using basic_format_string = basic_fstring<Char, T...>;template <typename... T>
using format_string = typename fstring<T...>::t;auto runtime(string_view s) -> runtime_format_string<>;
創建一個運行時格式字符串。
// 在運行時而不是編譯時檢查格式字符串。
fmt::print(fmt::runtime("{:d}"), "I am not a number");
2.4 命名參數(Named Arguments)
template <typename Char, typename T>
auto arg(const Char* name, const T& arg) -> detail::named_arg<Char, T>;
返回一個用于格式化函數的命名參數。它只能在調用格式化函數時使用。
fmt::print("The answer is {answer}.", fmt::arg("answer", 42));
目前,編譯時檢查不支持命名參數。
2.5 類型擦除
你可以創建自己的具有編譯時檢查和較小二進制體積的格式化函數,例如:
#include <fmt/format.h>void vlog(const char* file, int line,fmt::string_view fmt, fmt::format_args args) {fmt::print("{}: {}: {}", file, line, fmt::vformat(fmt, args));
}template <typename... T>
void log(const char* file, int line,fmt::format_string<T...> fmt, T&&... args) {vlog(file, line, fmt, fmt::make_format_args(args...));
}#define MY_LOG(fmt, ...) log(__FILE__, __LINE__, fmt, __VA_ARGS__)MY_LOG("invalid squishiness: {}", 42);
注意,與完全參數化的版本相比,vlog
沒有對參數類型進行參數化,這提高了編譯速度并減小了二進制代碼大小。
template <typename Context, typename... T, int NUM_ARGS, int NUM_NAMED_ARGS, unsigned long long DESC>
constexpr auto make_format_args(T&... args) -> detail::format_arg_store<Context, NUM_ARGS, NUM_NAMED_ARGS, DESC>;
構造一個存儲對參數的引用的對象,并且該對象可以隱式轉換為 format_args
。Context
可以省略,在這種情況下它默認為 context
。
template <typename Context>
class basic_format_args;void vlog(fmt::string_view fmt, fmt::format_args args); // 正確
fmt::format_args args = fmt::make_format_args(); // 懸空引用
格式化參數集合的視圖。為了避免生命周期問題,它應該僅用作類型擦除函數(如 vformat
)中的參數類型:
constexpr basic_format_args(const store<NUM_ARGS, NUM_NAMED_ARGS, DESC>& s);
從 format_arg_store
構造一個 basic_format_args
對象。
constexpr basic_format_args(const format_arg* args, int count, bool has_named);
從動態參數列表構造一個 basic_format_args
對象。
auto get(int id) -> format_arg;
返回具有指定 id
的參數。
using format_args = basic_format_args<context>;template <typename Context>
class basic_format_arg;auto visit(Visitor&& vis) -?> decltype(vis(0));
根據參數類型調用適當的 visit
方法來訪問參數。例如,如果參數類型是 double
,則將使用 double
類型的值調用 vis(value)
。
2.6 兼容性
template <typename Char>
class basic_string_view;
basic_string_view
是針對 C++17 之前版本實現的 std::basic_string_view
。它提供了該類型 API 的一個子集。即使存在 std::basic_string_view
,fmt::basic_string_view
也會被用于格式字符串,這樣做是為了防止在庫和客戶端代碼使用不同的 -std
選項進行編譯時出現問題(不推薦使用不同的 -std
選項)。
constexpr basic_string_view(const Char* s, size_t count);
從一個 C 字符串和一個大小構造一個字符串引用對象。
basic_string_view(const Char* s);
從一個 C 字符串構造一個字符串引用對象。
basic_string_view(const S& s);
從一個 std::basic_string
或 std::basic_string_view
對象構造一個字符串引用。
constexpr auto data() -> const Char*;
返回指向字符串數據的指針。
constexpr auto size() -> size_t;
返回字符串的大小。
using string_view = basic_string_view<char>;