说起格式化输出那真是一言难尽,从汇编到C,到c++,到c#、Java、Python、Go…哪个不是各出手段,尽显风骚。搞c++开发的,好多人其实用的不外乎是printf。可是这个函数有个致命的问题,它只能打印他自己能玩儿的东西,想换个他不知道的,好,做梦吧。
而c++在后来也觉得它用着不爽,毕竟我是带类的C,你不能打印类,那不等于大多数的工作没法完成。没办法,只好自己搞了个流式输入输出,重载一下operator<<,小日子和美美的过下去了。然而,肯定会有然而。
然而就是,STL中有大量的容器,可你不支持,举个简单例子,有一个std::vector,直接<
printf和流的就不用再写了,大家都用得非常熟稔。先从c++20的std::format开始。
先看一个cppreference.com上的例子:
#include
#include
#include
#include
template
std::string dyna_print(std::string_view rt_fmt_str, Args&&... args) {
return std::vformat(rt_fmt_str, std::make_format_args(args...));
}
int main() {
std::cout << std::format("Hello {}!\n", "world");
std::string fmt;
for (int i{}; i != 3; ++i) {
fmt += "{} "; // constructs the formatting string
std::cout << fmt << " : ";
std::cout << dyna_print(fmt, "alpha", 'Z', 3.14, "unused");
std::cout << '\n';
}
}
它的输出为:
Hello world!
{} : alpha
{} {} : alpha Z
{} {} {} : alpha Z 3.14
在std::formatter中默认已经对基本的数据类型进行了处理,但要是处理类似于自定义类的方式有两种方式来解决,一种是继承std::formatter来实现;另外一种就是自己实现parse和format两个函数。
看一下前者的例子:
先看cppreference提供的:
#include
#include
// 类型 T 的包装
template
struct Box
{
T value;
};
// 能用被包装值的格式说明格式化包装 Box
template
struct std::formatter, CharT> : std::formatter
{
// 从基类继承 parse()
// 通过以被包装值调用基类实现定义 format()
template
auto format(Box t, FormatContext& fc)
{
return std::formatter::format(t.value, fc);
}
};
int main()
{
Box v = {42};
std::cout << std::format("{:#x}", v);
}
再照着来一个:
template
struct ABC {
_T1 v1;
_T2 v2;
};
template
struct std::formatter, _CharT> : std::formatter<_T1, _CharT>
{
template
auto format(const ABC<_T1, _Ty2>& v, _FormatContext& format_context )
{
auto it = std::formatter<_T1, _CharT>::format(v.v1, format_context);
it = '\n';
it = std::formatter<_T2, _CharT>().format(v.v2, format_context);
return it;
}
};
#include
void Test()
{
ABC abc{
.v1 = 1,
.v2 = 2.1
};
std::cout << std::format("abc = {}", abc);
}
int main()
{
Test();
return 1;
}
第二种大家有兴趣可以自己搞搞,没觉得有如operator<<简单。
更多请参看:
https://zh.cppreference.com/w/cpp/utility/format/formatter
而在新标准中,对关联容器和非关联容器都有不同的处理方式,一般来说关联容器比较简单,直接输了就可以了,但对于非叛逆容器,如Map和Set系列等,可能对Ranges的输了不感冒,那么就需要象上面一样自己来定义一些输出格式。另外对于一些特殊形式,比如char数组到string,一些无法打印的类型这些都需要进行处理。最后,大家都知道,在STL还有一种容器适配器的存在,它们怎么处理?
所以在目前提出的解决方式中提出了规范:需要输入Range,元素类型可格式化并且元素类型不能是Range自己。当然还对视图以及一些引用形式及容器适配器都做了规范。比如默认就是格式化成[item0,item1…],使用方括号。而std::pair,std::tuple则是使用小括号,其它的可以参看最新的标准出来。
除此之外,std::println还提供了一些类似于正则内部的一些参数,可以去除一些内容,比如?可以忽视的调试符,n,去除外包装(就是外面那对括号),诸如种种,以最终标准及编译器厂商为准。
看一个例子:
std::println("{:?}", std::pair{3, 3}); // (3, 3)
std::println("{:n}", std::pair{3, 3}); // 3, 3
std::println("{:m}", std::pair{3, 3}); // 3: 3
同样,这些都是对于STL自带Ranges的容器,如果是自定义的呢?还是需要std::range_formatter来自定义元素进行格式化。My God!
反正在这个方面上,可能看上去简单了,但目前还没法体现出简单来。估计大佬们也在挠头。让他们去慢慢折腾吧,等着最后的结果就行。能用就用,不能用就暂时绕着呗。反正c++23到工程上用,肯定还有一大段时间。目前工程上能用到c++17的就已经是很潮的公司了。