本文仍为草稿

原文地址: https://hackingcpp.com/cpp/libs/fmt.html

前言

Formatting & Print 库(fmt库)是一个可以高效且安全地将 C 标准输入输出库转化为 C++ 输入输出流的现代 C++ 库(C++20 出现, C++23 加入标准库)

相比之前废拉不堪的输入输出函数, Formatting & Print 库有如下特性:

  • 更快
  • 更便捷
  • 类型安全

由于目前大多数编译器并没有实现对 C++23 中 print 库的支持, 我们依然使用 C++20 的 fmt 库演示格式化与输出操作

PTA 的 C++ 编译器为 gcc 11.4.0, 只支持到 C++17, 所以在 PTA 平台上用不了这么好用的东西

 

前期准备

由于 fmt 库不存在于 C++20 的标准库中, 所以我们要手动引入

VSCode with mingw

  1. 首先, 删除.vscode配置文件, 在 IDE 界面按下 F5 查看 VSCode 使用的编译器的路径

  2. 在任意空文件夹下运行下面的 git 命令获取 fmt 库

    git clone https://github.com/fmtlib/fmt.git
  3. 将获取到的代码中, 路径...\include\下的fmt\文件夹整个复制到编译器的库文件夹中, 对于我的配置来说, 路径是 C:\mingw64\lib\gcc\x86_64-w64-mingw32\13.1.0\include\c++\

    C:.
    ├─bits
    └─fmt

完成上述步骤后, 安装 fmt 库就完成了, 其他系统或编译器请参考: https://hackingcpp.com/cpp/libs/fmt.html

假如你的 Windows 额外安装了 VS, 那么你的 VSCode 的 C/C++ 配置很可能将 IntelliSense 的路径设置在了 VS 的 C++ 编译器, 导致在使用 fmt 的时候, VSCode 仍然提示错误波形曲线(然而并不影响编译运行)

可以在 C/C++ 配置中修改编译器为你常用的编译器

头文件

在代码前添加以下内容即可使用 fmt 库

#define FMT_HEADER_ONLY
#include<fmt/core.h>

 

总览

fmt 库最主要的两个函数是 fmt::format() 和 fmt::print()

fmt::format()

fmt::format() 用于格式化的构造一个字符串并返回

auto formattedString_1 = fmt::format("Vanadium\n");
auto formattedString_2 = fmt::format("{} years old\n", 24);
auto formattedString_3 = fmt::format("is a {}\n", "student");
std::cout<<formattedString_1<<formattedString_2<<formattedString_3;
/*output:
Vanadium
24 years old
is a student
*/

fmt::print()

fmt::print() 用于格式化的构造字符串并按规则输出

fmt::print("Hello, {}\n", "world");
fmt::print("ERROR {} / {}", 404, 403);
/*output:
Hello, world
ERROR 404 / 403
*/

 

构造规则

可以发现, fmt 库两个最重要的函数的共同点就是格式化的构造一个字符串, 因此我们来讨论控制字符串格式的方式

我们可以用以下的结构描述格式控制规则

  • id: 占位符的唯一标志
  • fill: 填充符
  • align: 靠左 / 靠右 / 居中
  • sign: 对于数字的符号
  • alt: 特殊控制
  • pad: 内边距
  • width: 宽度
  • prec: 保留小数位数

id

id 表示占位符向参数表的一个映射

int a, b, c;
a = 1, b = 2, c =3;
fmt::print("{1}, {1}, {0}, {2}", a, b, c);
// 2, 2, 1, 3

导入 fmt/format.h 库后, 可以利用函数 fmt::arg() 绑定 id 和内容

#include<fmt/format.h>

fmt::print("S = {pi} * {radius} * {radius}\n",
fmt::arg("pi", 3.14),
fmt::arg("radius", 6)
);
// S = 3.14 * 6 * 6

fill & align & width

width 用于控制输出内容的宽度, 单位为字符数, 其中缺少的字符由 fill (默认为’ ') 填充, 所输出的字符居于的位置由 align 控制, 默认为居左

char str[] = "Vagrant";
fmt::print("|{:10}|\n", str);
fmt::print("------------\n");
fmt::print("|{:*^10}|\n", str);
fmt::print("------------\n");
fmt::print("|{:#>10}|\n", str);
fmt::print("------------\n");
/*
|Vagrant |
------------
|*Vagrant**|
------------
|###Vagrant|
------------
*/

使用 fill 控制填充符的时候, 一定要使用 align 显式规定居于的位置, 否则会引发错误

Argument-Determined Width & Cut Width

fmt 支持参数化的控制输出内容的宽度和保留位数

char str[] = "ABCD";
std::vector<int> vec{1, 2, 3};
for(auto i:vec){
fmt::print("{:-<{}.{}}\n", str, 5 - i, i);
}
/*
A---
AB-
ABC
*/

fmt 不会自动切割输出内容以适应宽度, 需要使用 cut width 进行控制

类型特化

下面对于特定类型介绍一些输出或格式化方式

有符号数

有符号数可以用 sign 控制符控制符号的展示方式

  • +: 总是输出符号
  • -: 当且仅当负数输出符号(default)
  • (空格): 当且仅当负数输出符号, 但是为正数留出空格适应宽度
#include<fmt/format.h>
auto positive = 12;
auto zero = 0;
auto negative = -11.5;
fmt::print("{:-<+5}\n", positive);
fmt::print("{:-<+5}\n", zero);
fmt::print("{:-<+5}\n", negative);
fmt::print("-----\n");
/*
+12--
+0---
-11.5
-----
*/
fmt::print("{:-<-5}\n", positive);
fmt::print("{:-<-5}\n", zero);
fmt::print("{:-<-5}\n", negative);
fmt::print("-----\n");
/*
12---
0----
-11.5
-----
*/
fmt::print("{:-< 5}\n", positive);
fmt::print("{:-< 5}\n", zero);
fmt::print("{:-< 5}\n", negative);
fmt::print("-----\n");
/*
12--
0---
-11.5
-----
*/

此外, 有符号数可以用 alt 转换特殊进制的输出方式

auto num = 20;
auto width = 8;
fmt::print("{:>#{}x}\n", num, width);
fmt::print("{:>{}x}\n", num, width);
fmt::print("{:>#{}o}\n", num, width);
fmt::print("{:>{}o}\n", num, width);
fmt::print("{:>#{}b}\n", num, width);
fmt::print("{:>{}b}\n", num, width);
/*
0x14
14
024
24
0b10100
10100
*/

浮点数

利用 e / E 可以用科学计数法输出浮点数; f / F 表示以固定位数输出浮点数, 同时支持 INF

double data = 123.456, INF = 1.0 / 0;
fmt::print("{:e}\n", data);
fmt::print("{:E}\n", data);
fmt::print("{:f}\n", data);
fmt::print("{:f}\n", INF);
fmt::print("{:F}\n", INF);
/*
1.234560e+02
1.234560E+02
123.456000
inf
INF
*/

fmt 支持以十六进制输出浮点数

fmt::print("{:a}\n", data);
fmt::print("{:A}\n", data);
/*
0x1.edd2f1a9fbe77p+6
0X1.EDD2F1A9FBE77P+6
*/

C++ 采用 IEEE754 标准表示浮点数, 即

  • 1 位符号码
  • 11 位指数码
  • 52 位浮点数码

上文中, data 表示为 IEEE754 标准即为(其中浮点数码用十六进制表示):

0 00000000110 EDD2F1A9FBE77

指针

利用 fmt 格式化输出指针需要利用 fmt::ptr() 函数将指针转化为可被识别的类型

#include<fmt/format.h>
int n = 1;
auto const p = fmt::ptr(&n);
fmt::print("{:p}\n", p);
fmt::print("{:p}\n", fmt::ptr(&n));
/*
0x23a5bff6dc
0x23a5bff6dc
*/

STL 容器

fmt 可以很方便的输出 STL 容器

#include<fmt/ranges.h>
std::vector<int> vec{1, 3, 5, 7};
fmt::print("|{}|\n", vec);
std::tuple<int, char, double> tup{4, 'C', 6.6};
fmt::print("|{}|\n", tup);
std::map<int, char> map{{2, 'H'}, {6, 'P'}, {4, 'M'}};
fmt::print("|{}|\n", map);
std::set<int> set{1, 6, 2, 9, 7, 0, -6};
fmt::print("|{}|\n", set);
/*
|[1, 3, 5, 7]|
|(4, 'C', 6.6)|
|{2: 'H', 4: 'M', 6: 'P'}|
|{-6, 0, 1, 2, 6, 7, 9}|
*/

yysy, 这整的也太像 Python 了

当然, 很多时候我们并不希望输出奇怪的 list 的框, tuple 的括号, dictionary 的花括号, fmt 为我们提供了 fmt::join() 函数联结容器和分隔符

std::set<int> set{1, 6, 2, 9, 7, 0, -6};
fmt::print("|{:2> }|\n", fmt::join(set, "#"));
// |-6# 0# 1# 2# 6# 7# 9|
var_set = set([1, 6, 2, 9, 7, 0, -6])
print(var_set)
print("#".join(map(str, var_set)))
# {0, 1, 2, 6, 7, 9, -6}
# 0#1#2#6#7#9#-6

这样更像 Python 了, (叹气

使用 fmt::join() 后, {} 中的格式控制符会依次施加到每一个元素上

自定义

fmt 对自定义类型和容器的输出和格式化均有支持

自定义类型

这一段是直接搬运自原文的, 因为我试图运行原文提供的代码发现报错, 怀疑原因是新版本的 fmt 修改了对自定义类型格式化输出的逻辑(要求必定调用 fmt::formatter())

#include <fmt/ostream.h>

struct point2d {
int x;
int y;
point2d(int x_, int y_): x(x_), y(y_) {}
friend std::ostream& operator << (std::ostream& os, point2d const& p) {
return os << '(' << p.x << ',' << p.y << ')';
}
};

std::string s = fmt::format("Position is {}", point2d{7,9});
// s = "Position is (7,9)"

经过测试, 我们需要通过定义 fmt::formatter() 函数才能格式化输出我们自定义类型

#define FMT_HEADER_ONLY 
#include<fmt/core.h>
#include<fmt/ranges.h>
#include<iostream>
#include<fmt/format.h>

struct Point{
double x, y;
};

template<>
class fmt::formatter<Point>{
char preStyle = 'f';
public:
constexpr auto parse(format_parse_context& ctx){
auto i = ctx.begin(), end = ctx.end();
if(i != end && (*i == 'f' || *i == 'e')){
preStyle = *i++;
}
if(i != end && *i != '}'){
throw format_error("Invalid Format");
}
return i;
}
template<typename FmtCtx>
constexpr auto format(Point const& point, FmtCtx& ctx)const{
switch(preStyle){
default:
case 'f': return format_to(ctx.out(), "({:f}, {:f})", point.x, point.y);
case 'e': return format_to(ctx.out(), "({:e}, {:e})", point.x, point.y);
}
}
};

int main(){
Point point{2.3, 3.4};
fmt::print("{}\n", point);
fmt::print("{:f}\n", point);
fmt::print("{:e}\n", point);
}
/*
(2.300000, 3.400000)
(2.300000, 3.400000)
(2.300000e+00, 3.400000e+00)
*/

在这个程序中, 我们对 Point 进行特化, 首先定义了解析函数 fmt::formatter::parse(), 我们设想的 Point 类有 e / f 两种格式控制符, 然后定义了格式化函数 fmt::formatter::format() , 按照列别进行格式化并返回

自定义容器

fmt 对自定义容器的输出和格式化提供支持, 对于容器来说, 满足以下条件即可被格式化:

  • fmt 对容器内的所有类型均有支持
  • 容器提供了 T::begin() 和 T::end() 成员函数以获得迭代器
#define FMT_HEADER_ONLY 
#include<fmt/core.h>
#include<fmt/ranges.h>
#include<iostream>
#include<fmt/format.h>
#include<vector>

class IntVec{
std::vector<int> vec;
public:
IntVec(std::initializer_list<int> list):vec(list){}
auto begin(){
return vec.begin();
}
auto end(){
return vec.end();
}
};

int main(){
IntVec ivec{1, 3, 5, 7, 9};
fmt::print("{}\n", ivec);
}
// [1, 3, 5, 7, 9]

假如我们利用自定义类型 Point 定义自定义容器 Graph

class Graph{
std::vector<Point> pointVec;
public:
Graph(std::initializer_list<double> list){
for(auto iter = list.begin(); iter < list.end(); iter += 2){
pointVec.push_back(Point{*iter, *(iter + 1)});
}
}
auto begin(){
return pointVec.begin();
}
auto end(){
return pointVec.end();
}
};

int main(){
Graph grf{1, 2, 3, 4, 5, 6};
fmt::print("{}\n", grf);
}
// [(1.000000, 2.000000), (3.000000, 4.000000), (5.000000, 6.000000)]

文件输出

调用 cstdio 可以实现输出内容到文件

#define FMT_HEADER_ONLY 
#include<fmt/core.h>
#include<vector>
#include<cstdio>

int main(){
std::vector<int> vec{1, 2, 3, 4};
std::FILE* file = std::fopen("fmt.out", "w");
for(auto i:vec){
fmt::print(file, "Line {}\n", i);
}
std::fclose(file);
}
Line 1
Line 2
Line 3
Line 4

后记

目前还有很多东西无法在本地复现, 大多是因为环境配置和 fmt 的版本的问题, 目前就先这样吧 XD