第64例:fmt库——集成C++格式化库

说实话,C++标准库里的 iostreamprintf 家族,用起来总有点别扭。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::formatfmt::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 集成 fmt FetchContent find_package add_subdirectory 自动下载,版本锁定 系统安装,复用库 离线环境,完全可控 ⭐ 最推荐

说白了,集成 fmt 库就三步:选方式、写 CMake、链库。我个人建议新手直接上 FetchContent,等项目大了再考虑用包管理器管理依赖。

好了,这一讲就到这里。fmt 库虽然小,但用好了能极大提升代码的可读性和安全性。下次写日志、拼接字符串的时候,试试用 fmt::format 替代 cout 吧。


公众号:蓝海资料掘金营,微信deep3321