☰
BusTub 中的 {fmt} 格式化库 API 实战指南:从基础格式化到用户自定义类型扩展
2026/10/15 11:40:34 网站建设 项目流程
  • 数据库
  • 教育
  • 后端

【免费下载链接】bustub

The BusTub Relational Database Management System (Educational)

项目地址:https://gitcode.com/gh_mirrors/bu/bustub
点击查看免费下载

{fmt} 是一个开源的高性能、类型安全的 C++ 格式化库,本篇文章以 BusTub 数据库管理系统仓库(third_party/fmt/doc/api.md)中的 API 参考文档为核心,系统梳理其全部 API 组件、格式化函数、自定义类型扩展机制与编译期检查能力,并结合 BusTub 源码中的真实集成方式与调用实践进行纵深讲解。读完本文,你将掌握 {fmt} 从fmt::print到formatter特化、fmt::join、FMT_COMPILE等进阶用法的完整知识,并能在自己的 C++ 项目中直接复用这些实战模式。

一、{fmt} 在 BusTub 中的角色与 API 组成总览

BusTub 是卡内基梅隆大学数据库组维护的教学型关系数据库管理系统(Relational Database Management System),其源码在 binder、executor、catalog 等模块中广泛依赖 {fmt} 完成字符串拼接、计划节点(PlanNode)描述、错误信息格式化等工作。{fmt} 以第三方库形式随仓库一起构建:

  • 顶层 CMakeLists.txt 将third_party/fmt/include加入全局头文件搜索路径;
  • third_party/CMakeLists.txt 通过add_subdirectory(fmt)将其纳入构建体系;
  • 源码中大量使用fmt::format、fmt::print、fmt::join,例如 src/execution/fmt_impl.cpp、src/binder/fmt_impl.cpp 等。

{fmt} 的 API 由以下组件构成(全部函数与类型位于fmt命名空间,宏统一以FMT_为前缀):

头文件职责
fmt/base.h基础 API:面向char/UTF-8 的核心格式化函数,自带 C++20 编译期检查,依赖最小、编译更快
fmt/format.h完整格式化 API:fmt::format等函数以及 locale 支持
fmt/ranges.h范围(range)与元组(tuple)的格式化
fmt/chrono.h日期与时间格式化
fmt/std.h标准库类型的 formatter 特化
fmt/compile.h格式字符串编译(编译期生成格式化代码)
fmt/color.h终端颜色与文本样式输出
fmt/os.h系统 API(std::ostream输出、Windows 错误处理等)
fmt/ostream.hstd::ostream流式输出支持
fmt/args.h动态参数列表
fmt/printf.h类型安全的printf
fmt/xchar.h可选的wchar_t宽字符支持

说明:fmt/base.h只有在将 {fmt} 作为库(而非 header-only 模式)使用时才能体现其编译期收益;该头文件默认已为int、long long、unsigned、unsigned long long、float、double、long double、bool、char、const char*、fmt::string_view、const void*等类型提供formatter特化。

二、Base API:核心格式化函数

格式化函数采用与 Pythonstr.format相似的格式字符串语法:fmt是包含字面文本与{}花括号占位字段的格式字符串,占位字段在结果字符串中被替换为格式化后的参数;args是待格式化对象组成的参数列表。I/O 错误默认以std::system_error异常形式报告。

2.1 直接输出:print与println

// 输出到标准输出 fmt::print(format_string<T...>, T&&... args); fmt::println(format_string<T...>, T&&... args); // 自动追加换行 // 输出到指定文件流 fmt::print(FILE* f, format_string<T...>, T&&... args); fmt::println(FILE* f, format_string<T...>, T&&... args);

其底层实现是vprint/vprintln(声明见 third_party/fmt/include/fmt/base.h)。println相比print会在末尾自动补一个换行符。

2.2 写入迭代器:format_to、format_to_n与formatted_size

// 将格式化结果写入任意输出迭代器(如 std::back_inserter),返回迭代器 auto it = fmt::format_to(OutputIt&& out, format_string<T...>, T&&... args); // 限定最多写入 n 个字符,返回 format_to_n_result{out, size} auto result = fmt::format_to_n(OutputIt out, size_t n, format_string<T...>, T&&... args); // result.out 为写入末尾后的迭代器,result.size 为总字符数(未截断) // 仅计算格式化结果占用的字节数,不实际写入 size_t sz = fmt::formatted_size(format_string<T...>, T&&... args);

format_to_n适合需要受控写入的场景(如固定大小缓冲区),formatted_size常用于预先分配缓冲区大小。

2.3 类型安全的format_string与运行时字符串fmt::runtime

fmt::format_string<T...>是格式字符串类型,可从字符串字面量或constexpr字符串隐式构造,并在 C++20 下于编译期完成格式串与参数类型的匹配检查。若格式字符串只能在运行时获得,则必须用fmt::runtime显式包装以绕过编译期检查:

std::string s = fmt::format(fmt::runtime(runtime_fmt), 42);

2.4 BusTub 中的基础 API 实践

BusTub 各模块的ToString()实现是fmt::format的典型用法,例如 src/binder/fmt_impl.cpp 中绑定函数调用的描述:

auto BoundFuncCall::ToString() const -> std::string { return fmt::format("{}({})", func_name_, args_); }

src/execution/fmt_impl.cpp 中聚合计划节点的描述同样借助fmt::format一次拼接多个字段:

auto AggregationPlanNode::PlanNodeToString() const -> std::string { return fmt::format("Agg {{ types={}, aggregates={}, group_by={} }}", agg_types_, aggregates_, group_bys_); }

注意其中{{与}}是花括号的转义写法,这是 {fmt} 格式字符串语法的重要细节。

三、格式化用户自定义类型:format_as与formatter特化

{fmt} 已为大量标准 C++ 类型提供 formatter(范围/元组见fmt/ranges.h,日期时间见fmt/chrono.h,其余标准库类型见fmt/std.h)。让你的自定义类型可被格式化有两种途径:

3.1 轻量方案:format_as函数

如果自定义类型希望“借用”另一类型的格式说明符进行格式化,只需在与该类型相同的命名空间内提供一个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); // Output: 7 }

3.2 完整方案:特化formatter结构体模板

特化formatter能获得对解析与格式化流程的完全控制,需要实现两个方法:

template <> struct fmt::formatter<T> { // 解析格式说明符并存入 formatter。 // [ctx.begin(), ctx.end()) 是待解析的说明符字符区间, // 例如 fmt::format("{:f} continued", ...) 中区间内容为 "f} continued", // 解析到 '}' 或区间末尾为止,返回指向 '}' 的迭代器。 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; };

推荐做法是通过继承或组合复用已有 formatter,这样无需自行实现标准格式说明符。例如让color枚举复用string_view的格式化能力:

// 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从基类继承,因此自动识别全部字符串格式说明符。例如:

fmt::format("{:>10}", color::blue) // 返回 " blue"

(对同一类型同时提供formatter特化与format_as重载是不允许的。)

3.3 为类继承层次编写 formatter

借助 SFINAE 可以为一个类层次统一编写 formatter,子类对象通过基类指针格式化时也能输出正确的动态类型:

// 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); // Output: B }

3.4 BusTub 源码中的 formatter 特化实例

BusTub 正是以继承方式为表达式体系定制 formatter 的典范。在 src/include/binder/bound_expression.h 中,所有BoundExpression派生类与对应的std::unique_ptr<T>都通过继承fmt::formatter<std::string>获得格式化能力,格式化结果即ToString()的输出:

template <typename T> struct fmt::formatter<T, std::enable_if_t<std::is_base_of<bustub::BoundExpression, T>::value, char>> : fmt::formatter<std::string> { template <typename FormatCtx> auto format(const T &x, FormatCtx &ctx) const { return fmt::formatter<std::string>::format(x.ToString(), ctx); } };

同一文件(src/include/binder/bound_expression.h)还为枚举类型ExpressionType提供了继承formatter<string_view>的特化,将枚举值映射为可读的名称字符串("Constant"、"ColumnRef"、"AggregationCall"等)。这样fmt::format("{}", expr_type)便能在不编写大量分支的情况下直接输出人类可读的类型名——这正是 3.2 节继承式 formatter 模式的工程级落地。

四、编译期检查与类型擦除

4.1 编译期格式串检查

在支持 C++20consteval的编译器上,编译期格式字符串检查默认开启;旧编译器可通过FMT_STRING宏获得等效能力(要求 C++14 或更高,C++11 下为空操作)。未使用的参数与 Python 的str.format一样被允许。若希望强制使用FMT_STRING形式的旧式检查,可定义预处理宏FMT_ENFORCE_COMPILE_STRING,此时以普通字符串调用接受FMT_STRING的函数将编译失败。

4.2 类型擦除:自定义带编译期检查的格式化函数

通过fmt::format_string、fmt::vformat与fmt::make_format_args组合,可以编写自己的、带编译期检查且二进制体积更小的日志函数——vlog不对参数类型做模板化,因此能改善编译时间并减小代码体积:

#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);

关键类型包括:fmt::make_format_args(T&...)(打包参数)、basic_format_args/format_args(擦除后的参数列表)、basic_format_arg(单个擦除参数)。此外fmt/base.h还提供basic_format_parse_context、context、format_context等上下文类型,供 formatter 实现使用。

五、命名参数与基础兼容类型

fmt::arg(const Char*, const T&)用于命名参数,常用于本地化场景(如fmt::format("{name}", fmt::arg("name", "world")))。注意:命名参数目前不参与编译期检查。

兼容性方面,fmt/base.h提供basic_string_view与string_view,用于在不依赖std::string_view(或其缺失的环境)时完成字符串参数传递。

六、Format API:完整格式化功能与实用工具

fmt/format.h定义完整的格式化 API,在基础 API 之上增加格式化函数与 locale 支持。

6.1 核心函数

  • fmt::format(format_string<T...>, T&&...) -> std::string:将参数格式化为字符串。其实现(见 third_party/fmt/include/fmt/format.h)是return vformat(fmt.str, vargs<T...>{{args...}});,即所有参数先被擦除为统一表示后再进入vformat;
  • fmt::vformat(string_view, format_args) -> std::string:以已擦除参数列表进行格式化(见 third_party/fmt/include/fmt/format.h);
  • operator""_a:生成命名参数的字符串字面量操作符(位于fmt::literals命名空间)。

6.2 实用工具函数

函数作用
fmt::ptr(T)格式化指针地址(unique_ptr/shared_ptr版本位于fmt/std.h)
fmt::underlying(Enum)取枚举的底层整数值
fmt::to_string(const T&)用默认格式将任意可格式化对象转为std::string(整型重载使用栈上缓冲区,无动态分配,见 third_party/fmt/include/fmt/format.h)
fmt::group_digits(T)按千位分组输出数字
fmt::detail::buffer/fmt::basic_memory_buffer内存缓冲类型,basic_memory_buffer可在栈上预分配inline_buffer_size字节

6.3 系统错误处理

{fmt}不使用errno向用户传递错误,但它可能调用会设置errno的系统函数,因此用户不应假设库函数调用后errno的值被保留。fmt::system_error用于构造携带系统错误码的异常,fmt::format_system_error用于格式化系统错误消息。

6.4 自定义分配器

basic_memory_buffer的模板参数可指定自定义分配器,从而让格式化过程使用自定义内存管理:

using custom_memory_buffer = fmt::basic_memory_buffer<char, fmt::inline_buffer_size, custom_allocator>;

也可编写使用自定义分配器的格式化函数:将custom_memory_buffer与fmt::vformat_to(std::back_inserter(buf), fmt, args)配合,最后取出buf.data()与buf.size()构造自定义分配器管理的字符串。分配器仅用于输出容器;内置类型与字符串类型的格式化通常不会产生堆分配,仅非默认浮点格式化偶尔回退到sprintf。

6.5 Locale 支持

默认所有格式化与 locale 无关。要插入 locale 的数字分隔符,使用'L'格式说明符:

#include <fmt/core.h> #include <locale> std::locale::global(std::locale("en_US.UTF-8")); auto s = fmt::format("{:L}", 1000000); // s == "1,000,000"

fmt/format.h还提供接受std::locale参数的format、format_to、formatted_size重载。locale 类型作为模板参数传递,以避免引入昂贵的<locale>头文件。

七、范围与元组格式化(fmt/ranges.h)

fmt/ranges.h为范围(range)与元组提供格式化支持,包括std::vector等标准容器:

#include <fmt/ranges.h> fmt::print("{}", std::tuple<char, int>{'a', 42}); // Output: ('a', 42)

fmt::join用自定义分隔符连接元素,其重载包括:

  • fmt::join(Range&&, string_view):连接整个范围;
  • fmt::join(It, Sentinel, string_view):连接迭代器区间[It, Sentinel);
  • fmt::join(std::initializer_list<T>, string_view):连接初始化列表。
#include <fmt/ranges.h> auto t = std::tuple<int, char>{1, 'a'}; fmt::print("{}", fmt::join(t, ", ")); // Output: 1, a

BusTub 实践:在 src/execution/fmt_impl.cpp 的AbstractPlanNode::ChildrenToString中,子计划节点的多行文本先被逐行缩进,再通过fmt::join(children_str, "\n")以换行符连接成整体,配合fmt::format("\n{}", ...)输出——这是fmt::join在真实工程中处理多行文本拼接的典型用法:

return fmt::format("\n{}", fmt::join(children_str, "\n"));

八、日期与时间格式化(fmt/chrono.h)

fmt/chrono.h为以下类型提供 formatter:

  • std::chrono::duration
  • std::chrono::time_point
  • std::tm

格式语法详见 Chrono 格式说明。示例:

#include <fmt/chrono.h> int main() { std::time_t t = std::time(nullptr); fmt::print("The date is {:%Y-%m-%d}.", fmt::localtime(t)); // Output: The date is 2020-11-07. // (2020-11-07 替换为当前日期) using namespace std::literals::chrono_literals; fmt::print("Default format: {} {}\n", 42s, 100ms); // Output: Default format: 42s 100ms fmt::print("strftime-like format: {:%H:%M:%S}\n", 3h + 15min + 30s); // Output: strftime-like format: 03:15:30 }

辅助函数fmt::localtime(std::time_t)与fmt::gmtime(std::time_t)用于将时间戳转换为std::tm(分别为本地时间与 UTC 时间)。

九、标准库类型格式化(fmt/std.h)

fmt/std.h为以下标准库类型提供 formatter:

  • std::atomic、std::atomic_flag
  • std::bitset
  • std::error_code
  • std::exception
  • std::filesystem::path
  • std::monostate
  • std::optional
  • std::source_location
  • std::thread::id
  • std::variant

并提供了智能指针重载fmt::ptr(const std::unique_ptr<T, Deleter>&)与fmt::ptr(const std::shared_ptr<T>&)。

关于std::variant:只有当每一个 variant 备选类型都可格式化时,std::variant才可被格式化,且要求编译环境具备__cpp_lib_variant特性宏。示例:

#include <fmt/std.h> fmt::print("{}", std::variant<char, float>('x')); // Output: variant('x') fmt::print("{}", std::variant<std::monostate, char>()); // Output: variant(monostate)

位域与 packed 结构体的注意事项:要格式化位域成员或带有__attribute__((packed))的结构体字段,需先通过类型转换或一元+转换为底层或兼容类型:

struct smol { int bit : 1; }; auto s = smol(); fmt::print("{}", +s.bit);

这是 C++“完美转发”机制的已知限制,使用时应留意。

十、格式字符串编译(fmt/compile.h)

fmt/compile.h通过FMT_COMPILE宏或fmt::literals命名空间中的_cf用户自定义字面量提供格式字符串编译与编译期(constexpr)格式化。被FMT_COMPILE或_cf标记的格式字符串会在编译期完成解析、检查并转换为高效的格式化代码。该机制支持内置类型、字符串类型,以及format函数将格式上下文类型作为模板参数的用户自定义类型:

template <> struct fmt::formatter<point> { constexpr auto parse(format_parse_context& ctx); template <typename FormatContext> auto format(const point& p, FormatContext& ctx) const; };

需要注意:格式字符串编译可能产生比默认 API 更多的二进制代码,只推荐在格式化成为性能瓶颈的热点位置使用。

十一、终端颜色与文本样式(fmt/color.h)

fmt/color.h支持终端颜色与文本样式输出:

  • fmt::print(const text_style&, format_string<T...>, T&&...):带样式打印;
  • fmt::fg(detail::color_type):设置前景色;
  • fmt::bg(detail::color_type):设置背景色;
  • fmt::styled(const T&, text_style):为值附加样式。

例如fmt::print(fmt::fg(fmt::color::red), "danger: {}", 1)即可在支持 ANSI 颜色的终端上输出红色文本。

十二、std::ostream支持(fmt/ostream.h)

fmt/ostream.h提供std::ostream支持,包括格式化那些重载了插入操作符(operator<<)的用户自定义类型。要让某类型通过std::ostream可格式化,只需提供继承自ostream_formatter的 formatter 特化:

#include <fmt/ostream.h> struct date { int year, month, day; friend std::ostream& operator<<(std::ostream& os, const date& d) { return os << d.year << '-' << d.month << '-' << d.day; } }; template <> struct fmt::formatter<date> : ostream_formatter {}; std::string s = fmt::format("The date is {}", date{2012, 12, 9}); // s == "The date is 2012-12-9"

相关工具:fmt::streamed(const T&)强制以operator<<方式格式化对象;fmt::print(std::ostream&, format_string<T...>, T&&...)将格式化结果写入任意std::ostream。

十三、动态参数列表(fmt/args.h)

fmt/args.h提供dynamic_format_arg_store——一种构建者风格(builder-like)的 API,用于动态构造格式参数列表。当参数集合在运行时才能确定(如参数个数或类型不确定的解释器、脚本绑定场景)时非常有用:

#include <fmt/args.h> fmt::dynamic_format_arg_store<fmt::format_context> store; store.push_back(42); store.push_back("answer"); fmt::vprint("{} = {}", store);

十四、安全的printf(fmt/printf.h)

fmt/printf.h提供printf风格的格式化,采用 printf 格式字符串语法并支持 POSIX 位置参数扩展。与标准printf不同,fmt 版本完全类型安全:当参数类型与格式说明不匹配时会抛出异常,而不会产生未定义行为。

fmt::printf(string_view fmt, const T&... args); fmt::fprintf(std::FILE* f, const S& fmt, const T&... args); fmt::sprintf(const S& fmt, const T&... args); // 返回 std::string

十五、宽字符串支持(fmt/xchar.h)

可选的fmt/xchar.h提供wchar_t与异种字符类型支持,包括fmt::is_char(判断字符类型的特征类)、fmt::wstring_view、fmt::wformat_context与fmt::to_wstring(const T&)。

十六、与 C++20std::format的兼容性

{fmt} 几乎完整实现了 C++20 格式化库(std::format),仅存在以下差异:

  • 所有名称定义在fmt命名空间而非std中,以避免与标准库实现发生冲突;
  • 宽度计算不使用 grapheme clusterization(字素簇切分)——该功能已在独立分支中实现,但尚未合入主分支。

这意味着 {fmt} 用户可以在 C++20 标准库支持不完整的环境中提前获得std::format的等价能力,或作为标准库实现的对比与后备方案。

十七、在 BusTub 中的集成与工程实践总结

回顾 BusTub 对 {fmt} 的集成方式,可以提炼出三个可直接复用的工程模式:

  1. 作为第三方子项目构建:在 third_party/CMakeLists.txt 中add_subdirectory(fmt),并将 third_party/fmt/include 加入 include 路径(CMakeLists.txt),即可全局使用而无需额外链接配置;
  2. 统一为对象体系定制 formatter:在 src/include/binder/bound_expression.h 中,通过继承fmt::formatter<std::string>并以 SFINAE 约束基类,为整个BoundExpression派生体系一次性提供格式化能力,格式化结果与ToString()保持一致——这让fmt::format("{}", expr)与fmt::join(exprs, ", ")可以直接用于日志与调试输出;
  3. 面向可读性输出计划树:在 src/execution/fmt_impl.cpp 中组合fmt::format与fmt::join生成多行缩进的执行计划描述,展示了占位符转义({{/}})、容器格式化与join分隔符在真实数据库系统中的综合运用。

{fmt} 的完整格式字符串语法(对齐、宽度、填充、精度、进制、Llocale 说明符等)见仓库内文档 third_party/fmt/doc/syntax.md,与本文的 API 参考互为补充。掌握本文所述的 API 分层结构与自定义类型扩展机制,即可在 BusTub 或任何 C++ 项目中写出类型安全、可读且高性能的字符串格式化代码。

  • 数据库
  • 教育
  • 后端

【免费下载链接】bustub

The BusTub Relational Database Management System (Educational)

项目地址:https://gitcode.com/gh_mirrors/bu/bustub
点击查看免费下载

相关推荐

上一篇:使用 List.Item.Meta 快速构建 Arco Design 基础列表:头像、标题与描述的组合实战
下一篇:FlashList 仓库的 Agent 反馈闭环:用 analyze-feedback Skill 自动化沉淀 GitHub Actions 实战经验

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询