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

产生的由来

在之前的C++标准之中,如果你想格式化文本,你可以使用传统的printf函数或STL iostream库,但是这两者,各有优缺点。

printf函数继承自C语言,50多年的发展,已经让其很高效,灵活和方便。就是格式语法看起来有点晦涩,但习惯后感觉还行。

printf("Hello,%s\n",c_string);

printf的缺点就是弱类型安全。printf函数,使用C的可变参数模型将参数传递给格式化程序。如果正常运行那么会非常高效,但参数类型与其对应的格式说明符不匹配时,可能会产生严重问题。

STL的iostream库以可读性和运行时性能为代价确保了类型安全。iostream的语法不常见,但很简单易懂。

cout<<"Hello,"<

iostream的缺点在于语法和实现方面的复杂性,构建格式化字符串可能冗长而晦涩。许多格式操作符在使用后必须重置,非则会产生难以调试的级联格式错误。这个库的本身庞大而复杂,导致代码比printf等效代码大太多,速度也慢很多。

最终的结果是,C++程序员只能在者两种有缺陷的方法中选择一种。

format出现

新格式库位于头文件中。格式库基于Python3中的str.format()方法建模。格式字符串基本上与Python中的格式字符串相同,通常可以互换。下面有一些简单的例子。

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

template
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以外的函数),但是编写一个简单的print()函数并不难。在这一个过程中来了解一些格式库的工作方式。下面提供了print()函数使用格式库的简单实现

#include
#include
#include

template
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<

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{

  template
  constexpr auto parse(PraseContext& ctx){
     return ctx.begin();
  }

  template
  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》

你可能感兴趣的:(现代C++探索,c++)