- 数据库
- 教育
- 后端
【免费下载链接】bustub
The BusTub Relational Database Management System (Educational)
{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.h | std::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, aBusTub 实践:在 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::durationstd::chrono::time_pointstd::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_flagstd::bitsetstd::error_codestd::exceptionstd::filesystem::pathstd::monostatestd::optionalstd::source_locationstd::thread::idstd::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} 的集成方式,可以提炼出三个可直接复用的工程模式:
- 作为第三方子项目构建:在 third_party/CMakeLists.txt 中
add_subdirectory(fmt),并将 third_party/fmt/include 加入 include 路径(CMakeLists.txt),即可全局使用而无需额外链接配置; - 统一为对象体系定制 formatter:在 src/include/binder/bound_expression.h 中,通过继承
fmt::formatter<std::string>并以 SFINAE 约束基类,为整个BoundExpression派生体系一次性提供格式化能力,格式化结果与ToString()保持一致——这让fmt::format("{}", expr)与fmt::join(exprs, ", ")可以直接用于日志与调试输出; - 面向可读性输出计划树:在 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)
相关推荐
告别格式化困境:fmt库自定义类型扩展完全指南
告别格式化困境:fmt库自定义类型扩展完全指南 你是否还在为C++中自定义类型的格式化而烦恼?尝试过各种方法却依然无法优雅地输出用户定义类型?本文将带你一步掌握
标准库react-native-windows 内置 {fmt} 库 API 完全参考:格式化、日志与自定义类型
react native windows 内置 {fmt} 库 API 完全参考:格式化、日志与自定义类型 导读 本篇文章以 react native wind
跨平台前端彻底解决!fmtlib/fmt中用户自定义类型与编译时格式化(_cf)的兼容难题
彻底解决!fmtlib/fmt中用户自定义类型与编译时格式化 _cf 的兼容难题 fmtlib/fmt作为一款现代格式化库,以其高效、安全和易用性受到广大开发者
标准库
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考