C++ libfmt 实战: 高效简便的格式化库

libfmt 是一个现代化的 C++格式化库{fmt}, 具有以下关键特性:

  1. 安全性: 受 Python 格式化功能启发, {fmt}printf系列函数提供安全替代方案. 格式字符串错误在编译时就能被检测出来, 并且通过自动内存管理避免缓冲区溢出错误.
  2. 可扩展性: 默认支持格式化大多数标准类型, 包括容器, 日期和时间等. 例如, 能以类似 JSON 的格式打印std::vector. 同时, 用户也能让自定义类型支持格式化, 并进行编译时检查.
  3. 高性能: 在数值格式化方面, {fmt}iostreamssprintf快很多, 速度提升范围从百分之几十到 20 - 30 倍不等. 它尽量减少动态内存分配, 还能将格式字符串编译为优化代码.
  4. Unicode 支持: 在主流操作系统上, {fmt}通过 UTF-8char字符串提供可移植的 Unicode 支持. 默认情况下与区域设置无关, 但也可选择本地化格式化, 解决了标准库在这方面的一些问题.
  5. 编译速度快: 该库大量使用类型擦除技术来加快编译速度. fmt/base.h提供的 API 子集, 包含极少的依赖, 却足以替代所有*printf的使用场景. 使用{fmt}的代码编译速度通常比等效的iostreams代码快几倍, 虽然printf编译速度更快, 但差距正在缩小.
  6. 二进制体积小: 类型擦除技术还能防止模板膨胀, 使得每次调用生成的二进制代码更紧凑.
  7. 可移植性强: {fmt}代码库独立且小巧, 核心仅由三个头文件组成, 无外部依赖.

本文将介绍libfmt的安装以及使用教程. 让读者一文了解其功能.

安装教程

  1. 通过 vcpkg 安装. 如果对 vcpkg 不熟悉, 可以参考我的博客: Vcpkg 使用全攻略: 支持 VS Code, Visual Studio 和 CLion

    vcpkg install fmt

    接着在CMakeLists.txt中使用:

    find_package(fmt CONFIG REQUIRED)
    target_link_libraries(main.exe PRIVATE fmt::fmt)
  2. 通过 CMake FetchContent 方法. 如果您对 CMake 不是很熟悉, 可以参考我的博客: CMake 入门教程: 从基础到实践

    include(FetchContent)
    
    FetchContent_Declare(
    fmt
    GIT_REPOSITORY https://github.com/fmtlib/fmt
    GIT_TAG        e69e5f977d458f2650bb346dadf2ad30c5320281) # 10.2.1
    FetchContent_MakeAvailable(fmt)
    
    target_link_libraries(<your-target> fmt::fmt)
  3. 嵌入使用(Embed). 将libfmt仓库加入到自己的工程, 作为一个组件使用.

    1. 先克隆仓库到本地
    git clone --depth 1 https://github.com/fmtlib/fmt.git
    1. CMakeLists.txt中使用:
    add_subdirectory(fmt) # 加到当前的项目中
    target_link_libraries(<your-target> fmt::fmt)

使用教程

格式化方式

  1. 基础样例

    fmt 库采用{}做占位符. 与 Python 的格式化方式很接近.

    fmt::println("Hello {} from year {}", "World", 2025);
    // 输出: Hello World from year 2025
    
  2. 指定参数位置

    fmt::println("{0}+{0}={1}", 1, 2);              // 输出: 1+1=2
    fmt::println("{0}{1}{0} {1}{0}{1}", 'a', 'b');  // 输出: aba bab
    
  3. 整数格式化

    // 数用不同的基数表示整数, 类似printf中的 %d, %o
    fmt::println("decimal: {0:d}; hex: {0:x};  oct: {0:o}; binary: {0:b}", 42);
    // 输出: decimal: 42; hex: 2a;  oct: 52; binary: 101010
    
    // with 0x or 0 or 0b as prefix:
    fmt::println("decimal: {0:d}; hex: {0:#x};  oct: {0:#o}; binary: {0:#b}", 42);
    // 输出: decimal: 42; hex: 0x2a;  oct: 052; binary: 0b101010
    
    // 指定宽度
    fmt::println("{:#06x}", 0);  // 输出: 0x0000
    
  4. 浮点数格式化

    fmt::println("{:.{}f}", 3.14, 1);  // 输出: 3.1
    
    // Replacing % +f, % -f, and % f and specifying a sign :
    fmt::println("{:+f}; {:+f}", 3.14, -3.14);  // 输出: +3.140000; -3.140000
    
    // show a space for positive numbers
    fmt::println("{: f}; {: f}", 3.14, -3.14);  // 输出: " 3.140000; -3.140000"
    
    // show only the minus -- same as '{:f}; {:f}'
    fmt::println("{:-f}; {:-f}", 3.14, -3.14);  // 输出: 3.140000; -3.140000
    
  5. 对齐和宽度

    // 左对齐
    fmt::println("{:<30}", "left aligned");
    // 输出: "left aligned                  "
    
    // 右对齐
    fmt::println("{:>30}", "right aligned");
    // 输出: "                 right aligned"
    
    // 居中对其, 空格填充
    fmt::println("{:^30}", "centered");
    // 输出: "           centered           "
    
    // 居中对其, 使用'*'字符对齐
    fmt::println("{:*^30}", "centered");  // 使用 '*' 作为填充字符
    // 输出: "***********centered***********"
    
    // 动态宽度
    int width = 30;
    fmt::println("{:*^{}}", "centered", width);
    // 输出: "***********centered***********"
    
  6. 时间格式化

    std::tm datetime = {};
    datetime.tm_year = 2025 - 1900;            // 年份从 1900 开始计数
    datetime.tm_mon = 1 - 1;                   // 月份从 0 开始计数
    datetime.tm_mday = 30;                     // 日期
    datetime.tm_hour = 12;                     // 小时
    datetime.tm_min = 12;                      // 分钟
    datetime.tm_sec = 30;                      // 秒
    fmt::println("time: {:%H:%M}", datetime);  // 输出: time: 12:12
    fmt::println("datetime: {:%Y-%m-%d %H:%M:%S}", datetime);
    // 输出: datetime: 2025-01-30 12:12:30
    

    关于时间格式请参考: fmt syntax

  7. 画 Unicode 字符

    fmt::print(
        "┌{0:─^{2}}┐\n"
        "│{1: ^{2}}│\n"
        "└{0:─^{2}}┘\n",
        "", "Hello, world!", 20);

    输出:

    ┌────────────────────┐
    │   Hello, world!    │
    └────────────────────┘

本节的完整代码请参考basic.cpp

打印容器

  1. 打印标准容器

    // vector
    std::vector<int> vec = {1, 2, 3, 4, 5};
    fmt::println("Vector: {}", vec);  // 输出: Vector: [1, 2, 3, 4, 5]
    
    // array
    std::array<std::string, 3> arr = {"C++", "Python", "Rust"};
    fmt::println("Array: {}", arr);  // 输出: Array: [C++, Python, Rust]
    
    // map
    std::map<std::string, int> map = {
        {"Alice", 25}, {"Bob", 30}, {"Charlie", 20}};
    fmt::println("Map: {}", map);
    // 输出: Map: {"Alice": 25, "Bob": 30, "Charlie": 20}
    
    // set
    std::set<int> set = {3, 1, 4, 1, 5};
    fmt::println("Set: {}", set);  // 输出: Set: [1, 3, 4, 5]
    
  2. 嵌套容器

    std::vector<std::vector<int>> matrix = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
    fmt::println("Matrix: {}", matrix);
    // 输出: Matrix: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
    
  3. 格式化组合

    std::vector<double> numbers = {3.14159, 2.71828, 1.41421};
    fmt::println("Numbers: {::.2f}", numbers);  // Numbers: [3.14, 2.72, 1.41]
    
    fmt::println("Vector: {::#x}", vec);  // Vector: [0x1, 0x2, 0x3, 0x4, 0x5]
    

本节完整代码请参考: container.cpp


对传统代码的替代

性能对比

下图是官网给出的速度对比, 数值越小表示耗时越少.

speed

替换传统 API

// printf
// 用fmt::printf替换
fmt::fprintf(stderr, "Hello from %s!\n", "fmt::printf");
// 输出: Hello from fmt::printf!

// 改写
auto s = fmt::format("Hello from {}!", "fmt::format");
fmt::println(s);  // 输出: Hello from fmt::format!

// 与ostream的搭配
fmt::println(std::cerr, "Don't {}!", "panic");  // 输出: Don't panic!

// 替代 fstream
auto out = fmt::output_file("greeting.txt");
out.print("Hello {}!", "World");

// 替代: ostringstream
auto buffer = fmt::memory_buffer();
fmt::format_to(std::back_inserter(buffer), "width:{};", 480);
fmt::format_to(std::back_inserter(buffer), "height:{};", 360);
fmt::println(buffer.data());  // 输出: width:480;height:360;

本节完整代码请参考: stream.cpp

总结

libfmt作为一个现代的 C++ 格式化库, 其性能强大, 使用方便同时安全性高. 其不少特性已经被引入 C++标准库(容器部分尚未引入). 感兴趣的读者建议下载尝试.

参考链接

源码链接

本文示例源码链接