欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 新闻 > 焦点 > C++ 的 format 和 vformat 函数

C++ 的 format 和 vformat 函数

2024/12/22 0:22:55 来源:https://blog.csdn.net/orbit/article/details/144558298  浏览:    关键词:C++ 的 format 和 vformat 函数

1 C++ 字符串格式化的困境

1.1 “冗长的裹脚布”

​ 传统 C 库的 printf() 系列函数的优点就是函数调用更自然,并且格式化信息与参数分离,代码结构清晰,但是因为安全性问题一直被诟病。C++ 更推荐基于流的格式化输入输出代替传统 C 库的 printf() 系列函数,虽然解决了 printf() 系列函数的安全性问题,但是在易用性方面也是有点反人类。先不说输入和输出,单就说字符串的格式化,对 C++ 的使用者来说简直是“一把辛酸泪”。举个简单的例子,把浮点数格式化字符串,小数点后保留 3 位小数,用 sprintf() 实现非常简单:

char buf[64];
sprintf(buf, "%.3f", 1.0/3.0);  //buf 内是 0.333

如果用 C++,就是这样一番风格:

std::stringstream ss;
ss << std::fixed << std::setw(5) << std::setprecision(3) << 1.0/3.0;
std::string str = ss.str();  // str 是 0.333

​ 不得不说,C++ 的这个风格真的很“学院派”,各种控制 IO 流的操作符如果用来出题考试,定能让学渣们生不如死。这个例子只是格式化一个浮点数,如果要将多个不同类型的数据格式化到一个字符串中,需要多少个控制符拼接?像裹脚布,冗长且不直观。大多数 C++ 程序员对于格式化字符串不得不继续用 sprintf(),但是 sprintf() 除了安全性问题,还存在类型支持不足的问题。它只支持几种内置类型,不支持标准库中的各种容器,更不用说用户自定义的类型了。

1.2 C++ 11 的小革新

​ C++ 11 提供了一个新的 C 风格字符串格式化函数:

int snprintf(char* buffer, std::size_t buf_size, const char* format, ...);

除了 buf_size 参数有助于防止 buffer 溢出的好处之外,这个函数还可以计算对指定的参数进行文本格式化后需要的存储空间:

const char *fmt = "sqrt(2) = %f";
int sz = std::snprintf(nullptr, 0, fmt, std::sqrt(2));
std::vector<char> buf(sz + 1); // note +1 for null terminator
std::snprintf(&buf[0], buf.size(), fmt, std::sqrt(2));

借助于 C++ 11 的函数参数包(关于参数包可参考《C++ 的 “…”与可变参数列表》一篇),可以实现一个具有 C++ 风格的文本格式化函数。在 stackoverflow 网站,有人给出了这样一个解决方案:

template<typename ... Args>
std::string string_format(const std::string& format, Args ... args) {int size_s = std::snprintf(nullptr, 0, format.c_str(), args ...) + 1; // Extra space for '\0'if (size_s <= 0) { throw std::runtime_error("Error during formatting."); }auto size = static_cast<size_t>(size_s);std::unique_ptr<char[]> buf(new char[size]);std::snprintf(buf.get(), size, format.c_str(), args ...);return std::string(buf.get(), buf.get() + size - 1); // We don't want the '\0' inside
}//using string_format()
std::string s = string_format("%s is %d years old.", "Emma", 5);

虽然曲折,但是在你的编译器升级到 C++ 20 之前,还可一用。不过需要注意,虽然这个函数解决了溢出问题,但是类型安全问题仍然存在。

2 C++ 20 的 format 函数

2.1 format 函数

​ 虽然 boost 中的 format 库存在很长时间了,但是不知道是这个库的效率问题还是其他原因,一直没有入 C++ 标准委员会的法眼。很多 C++ 程序员对 Python 的 format() 函数“垂涎三尺”,fmtlib 库的出现终于缓解了这种渴望。更好的消息是 fmtlib 的一部分已经进入了 C++ 20 标准,比如 format() 函数和 vformat() 函数。来看几个 format() 函数的使用例子:

std::string name("Bob");
auto result = std::format("Hello {}!", name);  // Hello Bob!//03:15:30
result = std::format("Full Time Format: {:%H:%M:%S}\n", 3h + 15min + 30s);//***3.14159
std::print("{:*>10.5f}", std::numbers::pi);

是不是很像 Python?

2.2 fmt 标准格式

​ fmt 参数表示字符串格式化的格式,类型是 std::format_string(提案中最初版本是 std::string_view,后来是 std::format_string,与 C++ 23 补充的 std::print() 函数一致)。显然,这个“格式化字符串”是有格式的,一般来说,除了 “{” 和 “}” 两个特殊字符外,其他的字符都会原样复制到输出结果中。“{” 和 “}” 是表示格式的特殊符号,如果确实需要输出 “{” 和 “}”,就需要用转义序列 “{{” 和 “}}”代替。实际上一对 “{” 和 “}” 符号组成了一个占位符,这个占位符的语法描述是:

placeholder := '{' [arg-id][':' format-spec] '}'

其中 arg-id 和 format-spec 都是可选的,也就是说,一对空的大括号 “{}” 也是合法的格式字符串:

auto result = std::format("{} is {} years old.", "Kitty", 5);  //Kitty is 5 years old.

在这种情况下,每一对大括号与 args 表示的参数列表中的参数按顺序一一对应。如果 args 参数列表中的参数个数比 fmt 中的格式化占位符多,则不匹配的参数会被忽略,但不会报错。反过来,如果 args 参数列表中的参数个数比 fmt 中的格式化占位符少,则需要注意编译器的行为。资料 [9] 的 P2216 提案已经成为 C++ 23 的内容,所以 C++ 23 版本的编译器,会报编译错误。对于 C++ 20 版本的编译器,则要看它对这个提案的支持情况。还不支持 P2216 的编译器不会报编译错误,但是代码会在运行时抛出 format_error 异常。

auto result = std::format("{} is {} years old.", "Kitty", 5, 43.67);  //运行正常,43.67 被忽略
auto result = std::format("{} is {} years old.", "Kitty");  //取决于编译器

2.2.1 实参(占位符)索引(arg-id)

​ 如果格式化字符串中需要强调占位符与参数的位置关系,则需要指定 arg-id 参数。arg-id 用于指定占位符代表的格式化值在参数列表 args 中的下标。比如:

std::string dogs{ "dogs" };
std::string emma{ "Emma" };
auto result = std::format("{0} loves {1}, but {1} don't love {0}.", emma, dogs);
//Emma loves dogs, but dogs don't love Emma.

需要注意,实参索引要么都不使用,要么就全部指定,格式化字符串不支持部分使用索引的情况,比如这样的代码是错误的:

auto s = std::format("{1} is a good {}, but Dos is {0} !\n", 6.22, apple); //error

2.2.2 格式说明(format-spec)

​ 格式说明位于冒号的右边,它的语法形式是:

fill-and-align(optional) sign(optional) #(optional) 0(optional) width(optional) precision(optional) L(optional) type(optional) 		

看起来稍显复杂,但是无非就是填充、对齐、符号、域宽、精度等内容。首先看看填充和对齐(fill-and-align),填充字符可以是除了 “{” 和 “}” 之外的任意字符,紧跟在后面的就是对齐标志。对齐标志也是一个字符,用 “<” 表示强制左对齐,用 “>” 表示强制右对齐,对齐标志有三种,用 “^” 表示居中对齐,会在值的前面插入 ⌊ n 2 ⌋ \lfloor \frac{n}{2} \rfloor 2n 个填充字符,在值的后面插入 ⌈ n 2 ⌉ \lceil \frac{n}{2} \rceil 2n 个填充字符(注意取整方向)。

auto str = std::format("{:*<7}", 42); // str 的值为 "42*****"
auto str = std::format("{:*>7}", 42); // str 的值为 "*****42"
auto str = std::format("{:*^7}", 42); // str 的值为 "**42***"

如果不指定填充和对齐控制字符,则用默认的填充和对齐控制。默认的填充字符是空格,对于数字类型,默认的对齐控制是右对齐,对于字符串,默认的对齐控制是左对齐。

sign#0 用于数字的格式表达(都是可选),其中 sign 表示数字的符号,用 “+” 表示在非负数前面添加一个+号,用 “-” 表示在一个负数前添加一个负号,空格 “ ” 表示在非负数前面添加一个空格字符,在负数前面添加一个负号。关于符号需要注意两点:首先添加负号对于负数是默认行为,也就是说,即使不指定 sign 标志,输出负数时也会加一个负号。其次,如果数值是非负值,即使指定了 “-” 符号标志,也会被忽略,同样,对于负数,即使指定了 “+” 符号标志,输出时也会用负号代替。

auto s0 = std::format("{0:},{0:+},{0:-},{0: }", 1);   //"1,+1,1, 1"
auto s1 = std::format("{0:},{0:+},{0:-},{0: }", -1);  //"-1,-1,-1,-1"

# 用于转换输出数据的可替换形式,对于整数类型,除了默认的十进制形式,还可以用二进制、八进制和十六进制的形式表示数值。可以指定 type 控制符与 # 配合的方式指定数值的表示形式,比如 type 控制字符 “d” 表示十进制,这也是整数输出的默认形式,type 控制字符 “b” 表示二进制,会在数值前插入“0b” 两个字符。type 控制“o” 表示八进制,会在数值前插入一个字符 “0”。type 控制“x” 表示十六进制,会在数值前插入“0x” 两个字符,如果用大写的 “X” 字符,则插入 “0X” 两个字符。如果格式描述中指定了输出域宽,则表示替换形式的字符要跟在域宽数值后面,比如:

auto s = std::format("{0:#},{0:#6d},{0:#6b},{0:#6o},{0:#6x}", 10); //10,    10,0b1010,   012,   0xa

​ 对于浮点数类型,如果是有限数字,并且小数点后面没有有效数字的情况,比如 6.0,默认情况下是不输出小数点的。如果希望强制输出小数点,则需要用到 # 控制符,还可以配合 “g” 和 “G” 转换符在小数点后面补足 0,比如下面的代码:

auto s = std::format("{0:},{0:#},{0:#6g},{0:#6G}", 6.0); //6,6.,6.00000,6.00000

0 表示在数字前填充前导 0,对于无穷大和无效值,则忽略这个符号,不填充前导 0。如果 0 字符和对齐选项一起出现,则忽略 0 字符。看几个例子:

auto s = std::format("{:+06d}", 12);   // s 的值为 "+00012"
auto s = std::format("{:#06x}", 10); // s 的值为 "0x000a"
auto s = std::format("{:<06}", -42);  // s 的值为 "-42   " (因 < 对齐忽略 0 )

widthprecision 用于表示数字的域宽和精度。width 就是一个正的十进制数字,常用于配合对齐和填充控制符使用,也用于配合 0 控制符使用,前面已经用展示了相关的例子。precision 的形式就是小数点后面跟一个十进制数字或者跟一个嵌套的替换占位符表示。precision 只能用于浮点数或字符串,对于浮点数表示输出的格式化精度,对于字符串则表示使用字符串中多少个字符。type 控制字符 “f” 常用来配合精度控制使用,来看几个例子:

float pi = 3.14f;
auto s = std::format("{:10f}", pi);           // s = "  3.140000" (width = 10)
auto s = std::format("{:.5f}", pi);           // s = "3.14000" (precision = 5)
auto s = std::format("{:10.5f}", pi);         // s = "   3.14000"
auto s = std::format("{:>10.5}", "Kitty loves cats!");         // s = "     Kitty"

如果你觉得精度和宽度需要写死到格式化字符串中,会给使用带来不便,那你就太小看 C++ 标准委员会的专家了。格式说明部分支持嵌套格式化占位符的形式,动态指定相关的格式化参数,比如这样:

auto s = std::format("{:{}f}", pi, 10);       // s = "  3.140000" (width = 10)
auto s = std::format("{:.{}f}", pi, 5);       // s = "3.14000" (precision = 5)
auto s = std::format("{:{}.{}f}", pi, 10, 5); // s = "   3.14000" (width = 10, precision = 5)

在上面几行代码中,参数列表中表示精度的 10 和 5 可以直接指定,也可以是通过其他形式计算得到的动态结果,形式上非常灵活。但是使用嵌套定位符的形式,需要确保对应的参数是正整数类型,否则 std::format() 函数会抛出异常(C++ 23 会报编译错误,请参考资料 [9] 和 4.1 节的内容)。

L 控制符用于在格式化时引入地域化语言环境,这个控制符只用于算数类型,比如整数、浮点数和布尔类型数值的文本表示。对于整数,可按照本地语言环境插入适当的数位组分隔符。对于浮点数,也是按照本地语言环境插入适当的数位组和底分隔符。对于布尔类型的文本表示,与使用 std::numpunct::truename 和 std::numpunct::falsename 得到的结果一致。西方表达数字的习惯是用 “,” 做数字分隔符,中文环境则没有这个习惯,比如下面的代码,将地域化环境切换为西方英语环境,则数字的格式就有差别了:

std::locale::global(std::locale("en_US"));  //将语言环境切换为西方英语环境
auto s = std::format("{0:12},{0:12L}", 432198409L); // s =    432198409, 432,198,409

type 控制符用于确定数值以何种方式展现,前面介绍 # 控制符的时候已经介绍了 “o”、“b”、“d”、“x”、“X” 几个控制符。其实还有很多控制字符,比如 “B”,作用和 “b” 一样,只是数字前缀用 “0B” 两个字符。“e” 和 “E” 是用指数形式展示浮点数,“s” 用于输出字符串,“a” 和 “A” 是用 16 进制展示浮点数(用字母 p 表示指数)等等。

2.3 自定义格式

​ format 库的强大之处还在于对用户自定义类型扩展的支持,用户可以通过提供 std::formatter<> 模板的特化实现对自定义类型的支持。实际上,C++ 对标准库中的类型的支持,也是通过提供相应的 std::formatter<> 模板特化实现的,比如 char 类型的特化版本就是:

template<> struct formatter<char, char>;

如果要实现自定义数据类型的格式化规则,需要针对自定义数据类型实现 std::formatter<> 的特化版本。具体的实现方法请参考《C++ 的 format 函数支持自定义类型》。

3 std::vformat 和 std::format_args

​ std::format() 与 std::vformat() 的关系就如同 sprintf() 和 vsprintf() 的关系一样,主要用于用户自定的带格式化参数的函数与 format 库配合使用的场景,而 std::format_args 则用来配合进行参数传递。可以理解,std::vformat() 就是 std::format() 的一个类型擦除版本,为了配合 std::format() 的实现而存在,避免代码都在 std::format() 中造成的模板膨胀问题。一般不建议直接使用 std::vformat() 函数,4.2 节也介绍了在 C++ 26 之前直接使用 std::vformat() 可能导致的问题。但是在某些情况下,std::vformat() 函数还是有用武之地的,比如下面的例子。

​ 在 C++ 提供函数参数包之前,可变参数函数如果要配合 sprintf() 将用户输出的变长参数进行格式化,就需要用 va_list 配合 vsnprintf() 函数实现,比如这个记录日志的函数就是典型的用法:

void __cdecl DebugTracing(int nLevel, const char* fmt, ... ) {if(nLevel >= g_Level) { //控制日志记录的级别va_list args;va_start(args, fmt);int nBuf;char szBuffer[512];nBuf = _vsnprintf(szBuffer, sizeof(szBuffer)/sizeof(char), fmt, args);ASSERT(nBuf < sizeof(szBuffer)); LogMessage(szBuffer); //日志写入系统va_end(args);}
}

因为 … 不是具体的参数,所以无法直接调用 sprinf() 函数。只能用 va_start() 宏解析出 args 参数,然后调用 vsprintf() 函数。这个函数中 szBuffer[] 的使用其实是让人心惊胆战的,512 个字节的数组放在栈中是个不妥的设计,函数调用链层级比较深的话可能爆栈,此外,512 字节有时候可能还不够,动态申请内存显然很麻烦。

​ 对于现代 C++ 而言,可以 std::format_args 参数配合 std::vformat() 函数安全地实现这个功能:

void DebugTracing(int nLevel, const std::string_view& fmt, std::format_args&& args) {if (nLevel >= g_Level) { //控制日志记录的级别std::string msg = std::vformat(fmt, args);LogMessage(msg); //日志写入系统}
}

使用时可以借助 std::make_format_args() 函数帮忙构造 args 参数,比如:

DebugTracing(5, "{0:<d}{1:<x}", std::make_format_args(34, 42));

实际上,DebugTracing() 函数的使用可以借助函数参数包语法,将 args 替换成函数参数包,从而进一步简化使用:

template <typename... Args>
void DebugTracing(int nLevel, const std::string_view& fmt, Args&&... args) {if (nLevel >= g_Level) { //控制日志记录的级别std::string msg = std::vformat(fmt, std::make_format_args(args...));LogMessage(msg); //日志写入系统}
}

这样使用的时候就不需要 std::make_format_args() 了:

DebugTracing(5, "{0:<d}{1:<x}", 34, 42);

4 C++ 23 和 26 的持续优化

4.1 C++ 23 的改进

​ C++ 23 对 format 库的主要改进是支持更多的标准库类型,比如 Ranges[5]、thread::id、std::stacktrace 等等。资料[6] 讨论并明确了常见容器类型的格式化输出形式,比如:

  • map 类型: {k1: v1, k2: v2}
  • set 类型: {v1, v2}
  • 一般序列容器类型: [v1, v2]

在 C++ 20 没赶上火车的 std::print() 函数[4]也在 C++ 23 上车了,虽然还只支持标准输出流和文件流,但是已经具备了代替标准输入输出流的潜质。还解决了时间库的格式化在本地化处理时与 format 不兼容的问题,这个在《C++ 的时间库之八:format 与格式化》一篇已经介绍过了。另外,当使用 L 格式指定本地化输出的时候,format 的结果采用何种编码格式的问题,也由资料[8] 明确了,就是采用 unicode 编码格式,而不是系统默认的格式。这很重要,以中文为例,Windows 系统默认的格式是 GB2312、GB18030、GBK 等代表的扩展 ASCII 编码,而 Linux 采用的是 UTF-8 编码,如果库的规范在这个地方模糊,将给程序之间的数据交换带来隐患。

​ 资料 [9] 主要是对 LEWG 提出的一些问题进行了改进,比如将格式化字符串的类型从 basic_string_view 字符串类型改为 basic_format_string<charT, Args…> 类模板,改进的好处可以用这个例子说明一下:

auto s = std::format("{:d}", "I am not a number");

这行代码在 C++ 20 版本中会在运行时抛出一个 std::format_error 异常,但是改进后,就可以在编译期检查出参数类型的不匹配,因为 basic_format_string 类包含参数类型信息,可以检查格式化字符串的错误,这大大提升了 format() 函数的安全性。

​ 资料 [11] 的改进值允许 format 支持非常量可格式化类型。改进的原因是在 C++ 20 中 format() 函数声明大概是这个样子:

template<class... Args>
string format(string_view fmt, const Args&... args);//改进后大概这样:
template <class... Args>
string format(const format_string<_Types...> fmt, Args&&... args);

注意它对参数的要求是常量引用,也就是说,参数要么是带有 const,要么是可拷贝类型,这就限制了一些使用场景,比如非常量可迭代的 view,此时会产生临时对象,这种隐含的临时对象拷贝会产生不易觉察的开销。所以提案改进的结果就是改用转发引用,同时利用 format_string<> 对参数的生命周期进行检查。

​ 资料 [12] 和 [13] 主要是对填充字符的容差和长度估算问题给出了明确的解决方案,这几个问题分别是 LWG issue 3576、LWG issue 3639 和 LWG issue 3780,感兴趣的读者可通过 资料 [12] 和 [13] 的问题链接自行了解相关的情况。

4.2 C++ 26 的改进

​ C++ 26 的改进也不少,资料 [14] 解决了 to_string() 函数和 format() 函数输出数字的格式不一致问题,调整后的 to_string() 函数输出格式与 format() 函数的默认格式一致。资料 [15] 引入了一个很有意思的问题,来看这行代码:

format("{:>{}}", "hello", "10")

按说 C++ 23 已经支持对格式化字符串的检查,以上错误可以在编译期报错的,但是实际上不是,上述代码只是在运行期产生了一个 format_error 异常。我们在 2.2 节介绍过,format() 函数的格式化说明支持嵌套的形式使用动态格式参数,这样代码就是指定了一个动态宽度,按照顺序,下一个参数,也就是 “10” 就是这个动态宽度,但是显然,“10” 不是我们需要的整数类型。实际上根据第一个参数是字符串类型,编译器应该知道它的宽度是整数类型,所以这里应该能够检查出动态宽度部分对应的类型不正确问题。因此,P2757 提案就要求对格式化参数也进行类型检查。

​ 从 C++ 20 到 26,那么多数据类型都被支持,怎么能少了指针呢?本质上,指针作为整数类型,可以有多种输出格式,按照 16 进制格式输出就行了。但是,指针作为这么明确的一种数据类型,每次都要被 cast 成整数类型总是不爽。资料 [16] 就允许直接将指针格式化成地址形式:

//假设 uintptr_t 是预定义的指针类型
int i = 0;
format("{:#018x}", reinterpret_cast<uintptr_t>(&i)); // P2510 之前format("{:018}", &i);// P2510 之后

能少敲几次键盘也是极好的,对吧?

​ 前面介绍过,资料 [9] 提出了很多编译期检查的改进,但是对于字符串来说,由于资源限制,资料 [9] 没有提供一个好的 API 来使用格式字符串在编译期未知的格式化函数。作为一种变通方法,可以使用类型擦除(type-erased) API(std::vformat() 函数),但是这严重破坏了运行时的安全性。资料 [18] 建议引入 fmtlib 库现成的 runtime_format 提供运行期的检查,以避免不安全的代码破化系统安全性。资料 [9] 引入编译期检查的另一个问题就是要求格式化字符串必须是编译期可以求值的常量或立即函数的返回值,否则的话就会导致编译错误,比如:

std::string strfmt = translate("The answer is {}.");
auto s = std::format(strfmt, 42); // error

translate() 不是立即函数或常量函数,所以 strfmt 不是编译期常量,使用 format() 函数将导致编译错误。于是大家就想到了 vformat() 函数,但是这个类型擦除的 API 是为了避免模板膨胀而设计的,是给库或格式化函数开发者使用的。然而阿猫阿狗程序员朋友们被逼的没办法,也只能硬着头皮用了。但是,用的不合适,错误就来了,比如这个资料 [17] 上的例子:

std::string str = "{}";
std::filesystem::path path = "path/etic/experience";
auto args = std::make_format_args(path.string());
std::string msg = std::vformat(str, args);

这段看似“人畜无害”的代码隐含着 UB,因为格式化参数保存的是一个已经销毁的对象的引用。所以 [17] 建议将 make_format_args() 函数的参数从转发引用改成左值引用,从而避免错误使用右值的问题。

​ 资料 [19] 主要是解决 char 被当成整数类型处理的问题,当 char 遇到 d 或 x 格式化描述符时,会被当成整数处理,但是 char 的符号性却是由编译器实现决定的,嗯,问题就出来了。标准 ASCII 都是正值,但是遇到 unicode 编码的时候,就会遇到负值,这样的情况不同的编译器就会产生不一样的输出。所以资料 [19] 建议此时将 char 统一转型为 unsigned char,从而避免不一致问题。

​ 资料 [20] 引入了许多标准库类型的格式化支持,其中包括对文件库(filesystem)的 path 对象的格式化支持。但是因为带空格的字符串会有 quoting 字符的问题,那就是双引号,要不要转义?还有就是本地化面临的编码问题和格式问题,一度被 SG 16 要求移除对 path 的支持。资料 [21] 针对这些问题提出了一种改进的方案,总算解决了这个危机。


关注公众号,与作者互动:
在这里插入图片描述

参考资料

1.https://stackoverflow.com/questions/2342162/stdstring-formatting-like-sprintf

[2] P0645R10: Text Formatting

[3] P2372R3: Fixing locale handling in chrono formatters

[4] P2093R14: Formatted output

[5] P2286R8: Formatting Ranges

[6] P2585R1: Improve default container formatting

[7] P2693R1: Formatting thread::id and stacktrace

[8] P2419R2: Clarify handling of encodings in localized formatting of chrono types

[9] P2216R3: std::format improvements

[10] P2508R1: Expose std::basic-format-string<charT, Args…>

[11] P2418R2: Add support for std::generator-like types to std::format

[12] P2572R1: std::format() fill character allowances

[13] P2675R1: format’s width estimation is too approximate and not forward compatible

[14] P2587R3: to_string or not to_string

[15] P2757R3: Type-checking format args

[16] P2510R3: Formatting pointers

[17] P2905R2: Runtime format strings

[18] P2918R2: Runtime format strings II

[19] P2909R4: Fix formatting of code units as integers (Dude, where’s my char?)

[20] P1636R2: Formatters for librarytypes

[21] P2845R8: Formatting of std::filesystem::path

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com