产生的由来
在之前的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/3C++20通过提供高效.方便的类型安全文本格式库,解决了一个长期存在的问题。
参考书籍《C++20 cookbook》
C++20引入的format库解决了printf的类型安全问题和iostream的性能问题,提供了类似于Python的str.format()功能。format库基于类型安全的占位符和高效的实现,结合了iostream的可读性和printf的性能。它允许自定义类型格式化,如示例中的Frac结构体,通过特化std::formatter实现。
1922

被折叠的 条评论
为什么被折叠?



