原文地址: 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
首先, 删除.vscode
配置文件, 在 IDE 界面按下 F5 查看 VSCode 使用的编译器的路径
在任意空文件夹下运行下面的 git 命令获取 fmt 库
git clone https://github.com/fmtlib/fmt.git
将获取到的代码中, 路径...\include\
下的fmt\
文件夹整个复制到编译器的库文件夹中, 对于我的配置来说, 路径是 C:\mingw64\lib\gcc\x86_64-w64-mingw32\13.1.0\include\c++\
完成上述步骤后, 安装 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() 用于格式化的构造一个字符串并返回
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;
fmt::print()
fmt::print() 用于格式化的构造字符串并按规则输出
fmt::print ("Hello, {}\n" , "world" ); fmt::print ("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);
导入 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 ) );
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" );
使用 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); }
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" );
fmt::print ("{:-<-5}\n" , positive); fmt::print ("{:-<-5}\n" , zero); fmt::print ("{:-<-5}\n" , negative); fmt::print ("-----\n" );
fmt::print ("{:-< 5}\n" , positive); fmt::print ("{:-< 5}\n" , zero); fmt::print ("{:-< 5}\n" , negative); fmt::print ("-----\n" );
此外, 有符号数可以用 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);
浮点数
利用 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);
fmt 支持以十六进制输出浮点数
fmt::print ("{:a}\n" , data); fmt::print ("{:A}\n" , data);
C++ 采用 IEEE754 标准表示浮点数, 即
上文中, 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));
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);
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, "#" ));
var_set = set ([1 , 6 , 2 , 9 , 7 , 0 , -6 ]) print (var_set)print ("#" .join(map (str , var_set)))
这样更像 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 });
经过测试 , 我们需要通过定义 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); }
在这个程序中, 我们对 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); }
假如我们利用自定义类型 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); }
文件输出
调用 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); }
后记
目前还有很多东西无法在本地复现, 大多是因为环境配置和 fmt 的版本的问题, 目前就先这样吧 XD