产生的由来
在之前的C++标准之中,如果你想格式化文本,你可以使用传统的printf函数或STL iostream库,但是这两者,各有优缺点。
printf函数继承自C语言,50多年的发展,已经让其很高效,灵活和方便。就是格式语法看起来有点晦涩,但习惯后感觉还行。
printf("Hello,%s\n",c_string);
printf的缺点就是弱类型安全。printf函数,使用C的可变参数模型将参数传递给格式化程序。如果正常运行那么会非常高效,但参数类型与其对应的格式说明符不匹配时,可能会产生严重问题。
STL的iostream库以可读性和运行时性能为代价确保了类型安全。iostream的语法不常见,但很简单易懂。
cout<<"Hello,"<<str<<endl;
iostream的缺点在于语法和实现方面的复杂性,构建格式化字符串可能冗长而晦涩。许多格式操作符在使用后必须重置,非则会产生难以调试的级联格式错误。这个库的本身庞大而复杂,导致代码比printf等效代码大太多,速度也慢很多。
最终的结果是,C++程序员只能在者两种有缺陷的方法中选择一种。
format出现
新格式库位于<format>头文件中。格式库基于Python3中的str.format()方法建模。格式字符串基本上与Python中的格式字符串相同,通常可以互换。下面有一些简单的例子。
*
format()函数接受一个string_view格式的字符串和一个可变参数参数包,并返回一个字符串。其函数签名为:
template<typename...Args> string format(string_view fmt,const Args&...args);
*
format()返回类型或值的字符串表现形式。如下
string who{"everyone""}; int ival{42}; double pi{std::numbers::pi};
format("Hello, {}!\n",who); //Hello, everyone! format("Integer: {}\n",ival);
//Integer: 42 format("Π: {}\n",pi); //Π: 3.141592653589793
格式化字符串使用大括号{}作为类型安全的占位符,可以将任何兼容类型的值转换为合理的字符串表现形式
*
可以在格式字符串中包含多个占位符:
format("Hello {} {}",ival,who); //Hello 42 everyone
*
可以指定替换值的顺序
format("Hello {1} {0}",ival,who);//Hello everyone 42 format("Hello {0}
{1}",ival,who);//Hello 42 everyone
*
这也可以进行对齐,左(<),右(>)或中心(^)对齐,可以选择性使用填充字符:
format("{:.<10}",ival); //42........ format("{:.>10}",ival); //........42
format("{:.^10}",ival); //....42....
*
也可以设置十进制数值的精度
format("Π:{:.5}",pi); //Π: 3.1416
这是一个丰富而完整的格式化方式,具有iostream的类型安全,已经printf的性能和简单性,达到了鱼和熊掌兼得的目的
format的工作原理
format()函数本身返回一个字符串对象。若想打印字符串,需要使用iostream或cstdio
cout<<format("Hello,{}",who)<<endl; puts(format("Hello,{}",who).c_str());
这两种方法都不理想(毕竟还要调用除format以外的函数),但是编写一个简单的print()函数并不难。在这一个过程中来了解一些格式库的工作方式。下面提供了print()函数使用格式库的简单实现
#include<format> #include<string_view> #include<cstdio>
template<typename...Args> void print(const string_view fmt_str,Args&&...args){
auto fmt_args{make_format_args(args...)}; string
outstr{vformat(fmt_str,fmt_args)}; fputs(outstr.c_str(),stdout); }
注:make_format_args()函数的作用:接受参数包并返回一个对象,该对象包含适合格式化的已擦除类型的值。然后,将该对象传递给vformat(),vformat()再返回合适打印的字符串。再使用fputs()将值输出到控制台上。
现在可以使用print()函数,来代替cout<<format()的组合
print("Hello, {}!\n",who); print("Π: {}\n",pi); print("Hello, {1}
{0}!\n",ival,who); print("{:.^10}\n",ival); print("{:5}\n",pi);
输出为:
Hello, everyone! Π: 3.141592653589793 Hello everyone 42 ....42.... 3.1416
另外的类似的print()函数,这也是C++23计划的一部分。到时后编译器支持C++23的print()时,使用std::print就能完成所有工作.
format处理自定义类型
如下,这里有两个成员的简答结构体:分子和分母。将其输出为分数:
struct Frac{ long n; long d; } int main(){ Frac f{5,3}; print("Frac: {}\n",f);
}
编译时,会遇到如"没有定义的转换运算符..."等一系列错误.
当格式化系统遇到要转换的对象时,其会寻找具有相应类型的格式化程序对象的特化。因此我们也要建立一个对应自定义类型的特化。
template<> struct std::formatter<Frac>{ template<typename PraseContext& ctx>
constexpr auto parse(PraseContext& ctx){ return ctx.begin(); }
template<typename FormatContext> auto format(const Frac& f,FormatContext& ctx){
return format_to(ctx.out(),"{0:d}/{1:d}",f.n,f.d); } };
格式化特化,是具有两个简短模板模板函数的类
*
prase()函数解析格式字符串,从冒号之后(若没有冒号,则在开大括号之后)直到但不包括结束大括号。
*
format()函数接受一个Frac对象和一个FormatContext对象,返回结束迭代器。format_to()函数可使这变得很容易。先将f.n和f.d放入string_view即"{0:d}/{1:d}"中去,然后再将结果放入到目标格式化字符串中去。
现在有了Frac的特化,可以将对象传递print()从而获得一个可读的结果:
输出为
Frac: 5/3
C++20通过提供高效.方便的类型安全文本格式库,解决了一个长期存在的问题。
参考书籍《C++20 cookbook》