C++20格式化文本(format)探究

C++20引入的format库解决了printf的类型安全问题和iostream的性能问题,提供了类似于Python的str.format()功能。format库基于类型安全的占位符和高效的实现,结合了iostream的可读性和printf的性能。它允许自定义类型格式化,如示例中的Frac结构体,通过特化std::formatter实现。

产生的由来

在之前的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中的格式字符串相同,通常可以互换。下面有一些简单的例子。

  1. format()函数接受一个string_view格式的字符串和一个可变参数参数包,并返回一个字符串。其函数签名为:

template<typename...Args>
string format(string_view fmt,const Args&...args);
  1. 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

格式化字符串使用大括号{}作为类型安全的占位符,可以将任何兼容类型的值转换为合理的字符串表现形式

  1. 可以在格式字符串中包含多个占位符:

format("Hello {} {}",ival,who); //Hello 42 everyone
  1. 可以指定替换值的顺序

format("Hello {1} {0}",ival,who);//Hello everyone 42
format("Hello {0} {1}",ival,who);//Hello 42 everyone
  1. 这也可以进行对齐,左(<),右(>)或中心(^)对齐,可以选择性使用填充字符:

format("{:.<10}",ival);   //42........
format("{:.>10}",ival);   //........42
format("{:.^10}",ival);   //....42....
  1. 也可以设置十进制数值的精度

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);
  }

};

格式化特化,是具有两个简短模板模板函数的类

  1. prase()函数解析格式字符串,从冒号之后(若没有冒号,则在开大括号之后)直到但不包括结束大括号。

  1. 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》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Reol520

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值