第64例:fmt库——集成C++格式化库
说实话,C++标准库里的 iostream 和 printf 家族,用起来总有点别扭。cout << "hello " << name << " age " << age 这种链式写法,又长又容易漏掉 <<。而 printf 呢,类型不安全,格式化字符串里写 %d 还是 %s 全靠脑子记。
我个人习惯用 fmtlib(也就是 fmt 库)来解决这些问题。它提供了类似 Python 的 {} 占位符语法,类型安全,性能还比 sprintf 快。C++20 标准库里的 std::format,其实就是从 fmt 库移植过去的。
今天我们就聊聊,怎么在 CMake 项目里集成 fmt 库。
fmt 库是什么?
fmt 是一个轻量级的 C++ 格式化库。核心功能就一个:用花括号 {} 做占位符,把变量塞进字符串里。
举个例子:
#include <fmt/core.h>
int main() {
std::string name = "Alice";
int age = 30;
fmt::print("Hello, {}! You are {} years old.\n", name, age);
return 0;
}
输出:Hello, Alice! You are 30 years old.
你看,比 cout 简洁多了吧?而且类型安全——如果你传了个 std::string 给 {},它自动调用 .c_str() 或者直接输出字符串内容,不会像 printf 那样崩溃。
CMake 集成方式
集成 fmt 库,主要有三种方式。我按推荐程度排个序:
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| FetchContent | 大多数项目 | 自动下载,无需手动安装 | 首次构建需要联网 |
| find_package | 系统已安装 fmt | 复用系统库,节省编译时间 | 需要用户手动安装 |
| 源码拷贝 | 离线环境或定制编译 | 完全可控 | 维护麻烦 |
我个人最常用 FetchContent。为什么?因为省心。你想想看,让每个开发者去手动安装 fmt,万一版本不对呢?万一忘了装呢?用 FetchContent,CMake 自己就把事情办了。
方式一:FetchContent(推荐)
cmake_minimum_required(VERSION 3.14)
project(fmt_example LANGUAGES CXX)
include(FetchContent)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 10.2.1 # 指定版本,别用 master
)
FetchContent_MakeAvailable(fmt)
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE fmt::fmt)
嗯,这里要注意:GIT_TAG 一定要指定具体版本号,比如 10.2.1。我曾经见过有人写 main 分支,结果某天 fmt 库更新了 API,项目直接编译不过。那叫一个惨。
方式二:find_package
如果系统里已经装了 fmt,或者你想用包管理器(比如 vcpkg、conan)管理依赖,可以用这个:
find_package(fmt REQUIRED)
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE fmt::fmt)
前提是 fmt 已经安装到系统路径,或者通过 CMAKE_PREFIX_PATH 指定了安装位置。
方式三:源码拷贝
把 fmt 的源码直接放到项目 third_party/fmt 目录下:
add_subdirectory(third_party/fmt)
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE fmt::fmt)
这种方式适合离线环境,或者你需要修改 fmt 源码。但说实话,我很少这么干——升级版本时得手动替换整个目录,太麻烦了。
核心 API 速览
fmt 库的 API 设计得很清晰。我挑几个最常用的:
| 函数 | 作用 | 示例 |
|---|---|---|
fmt::format |
返回格式化后的字符串 | auto s = fmt::format("{} + {} = {}", 1, 2, 3); |
fmt::print |
直接输出到 stdout | fmt::print("Hello {}\n", "world"); |
fmt::println |
输出并自动换行(C++20 风格) | fmt::println("Hello {}", name); |
fmt::to_string |
将任意类型转为字符串 | auto s = fmt::to_string(42); |
格式化语法也很灵活:
// 指定宽度和对齐
fmt::print("{:<10} {:>10} {:^10}\n", "left", "right", "center");
// 数字格式化
fmt::print("{:.2f}\n", 3.14159); // 保留两位小数
fmt::print("{:06d}\n", 42); // 补零:000042
// 二进制、十六进制
fmt::print("{:b} {:x}\n", 255, 255); // 11111111 ff
避坑指南
集成 fmt 库时,有几个坑我踩过,分享给你:
如果你只需要 fmt::format 和 fmt::print,只包含 fmt/core.h 就够了。别一股脑 #include <fmt/format.h>,那个头文件会拖入大量模板代码,编译时间会变长。
用 fmt::fmt 命名空间别名可以少打字:
namespace f = fmt;
auto s = f::format("value: {}", 42);
我曾经在项目里用了 fmt 8.x,后来升级到 9.x,发现 fmt::format_to 的签名变了。所以建议锁定 GIT_TAG 版本,不要用 master。升级时先看 changelog。
完整示例:日志工具
最后,我写个实际点的例子。假设我们要做一个简单的日志工具:
#include <fmt/core.h>
#include <fmt/chrono.h> // 时间格式化
#include <chrono>
enum class LogLevel { INFO, WARN, ERROR };
void log(LogLevel level, const std::string& msg) {
auto now = std::chrono::system_clock::now();
std::string level_str;
switch (level) {
case LogLevel::INFO: level_str = "INFO"; break;
case LogLevel::WARN: level_str = "WARN"; break;
case LogLevel::ERROR: level_str = "ERROR"; break;
}
fmt::print("[{:%H:%M:%S}] [{}] {}\n", now, level_str, msg);
}
int main() {
log(LogLevel::INFO, "Application started");
log(LogLevel::WARN, "Disk space low");
log(LogLevel::ERROR, "Failed to open file");
return 0;
}
输出:
[14:30:25] [INFO] Application started
[14:30:25] [WARN] Disk space low
[14:30:25] [ERROR] Failed to open file
你看,fmt 的 chrono 扩展直接支持 std::chrono::system_clock::time_point 格式化,省去了写 strftime 的麻烦。
知识体系图
我把 fmt 库集成的核心逻辑画了张图,方便你理解整体脉络:
说白了,集成 fmt 库就三步:选方式、写 CMake、链库。我个人建议新手直接上 FetchContent,等项目大了再考虑用包管理器管理依赖。
好了,这一讲就到这里。fmt 库虽然小,但用好了能极大提升代码的可读性和安全性。下次写日志、拼接字符串的时候,试试用 fmt::format 替代 cout 吧。
公众号:蓝海资料掘金营,微信deep3321