在 C++20 之前,C++ 的格式化字符串相关设施一直饱受诟病。具体来说,在不依赖第三方库的情况下,有两种方式实现格式化字符串:
- 使用从 C 语言沿袭过来的
sprintf
以及相关函数; - 使用 C++ 的
std::stringstream
以及流相关设施。
然而,这两种方式都有明显的缺点。
对于第一种方式,即使用 sprintf
等函数进行格式化字符串,首先只能使用 C 风格字符串作为格式化结果,这意味着程序员需要手工管理存放字符串的内存空间;其次,sprintf
只能对有限的几种内置类型(例如整数、浮点数和 C 风格字符串等) 进行格式化,不支持也无法拓展支持格式化其他类型的数据;最后一点是 printf
系列函数一直存在的安全性问题,因为这些函数不是类型安全的。
对于第二种方式,不论是从语法上还是从本质上都与直接拼接字符串没有任何区别。在需要大规模进行字符串格式化的场景下,这种方式代码冗长、语法噪音极多,很不直观。
在 C++20 以前,除了 C/C++ 之外,几乎任意一个合格的现代命令式编程语言都在其标准库中实现有完备的格式化字符串设施。终于,在 C++ 社区长时间的呼吁声中,C++20 标准中加入了较为完备的格式化字符串支持,其核心函数为 std::format
。C++20 引入的格式化字符串在形式上借(chao)鉴(xi)了 Python 的格式化字符串形式,因此熟悉 Python 的同学能够很快地开始上手使用 C++20 的格式化字符串设施。
std::format
std::format
是一个函数模板,其原型如下:
template <typename ...Args> std::string format(std::string_view fmt, const Args&... args);
format
的第一个参数是格式串,剩余的参数是需要格式化的数据对象。在格式化串中,与 Python 类似,可以使用 {}
占位符表示输出串的对应位置由对应的数据对象格式化串进行替代。例如:
std::string name1("Alice"); std::string name2("Bob"); std::format("hello, {} and {}!", name1, name2); // hello, Alice and Bob!
{}
占位符中可以有一定的带有语法结构的内容,用于控制字符串格式化中的数据对象选择、格式化参数等信息。占位符的语法如下:
placeholder := '{' [index][':' format-spec] '}'
其中,index
是一个整数,用于选择使用哪一个数据对象进行格式化;format-spec
是数据对象类型相关的格式化参数,可以为任意字符串。有关 format-spec
以及如何为自定义类型指定格式化方法的内容将在下一节介绍。
使用 index
可以使多个占位符引用同一个数据对象:
std::string name("Alice"); std::format("hello, {0} and {0}!", name); // hello, Alice and Alice!
也可以使占位符在格式串中出现的顺序与数据对象在参数中的数据不同:
std::string name1("Alice"); std::string name2("Bob"); std::format("hello, {1} and {0}!", name1, name2); // hello, Bob and Alice!
需要注意到的是,在格式串中,所有的占位符要么都有 index
参数,要么都没有 index
参数。当所有的占位符都没有 index
参数时,占位符和数据对象将按照先后顺序一一匹配。如果某些占位符有 index
参数而另一些占位符没有 index
参数,则 std::format
函数会在运行时抛出 std::format_error
异常。
std::formatter
std::format
可以支持对自定义的数据类型进行格式化。为做到这一点,C++20 格式化字符串库提供了一个类模板 std::formatte<T, CharT>
专门负责根据格式占位符中的 format-spec
对某一个特定类型 T
的数据进行格式化操作。为了使 std::format
支持对自定义类型进行格式化,只需要定义一个 std::formatter
的特化模板,并实现一定的成员函数即可。STL 中已经为我们定义了整数、浮点数和字符串的 std::formatter
特化模板,因此我们可以直接对这些类型进行格式化。
当特化 std::formatter
模板时, 实现方需要在特化模板类中提供两个实例成员函数供 std::format
进行调用:parse
以及 format
函数。
parse
实例成员函数需要有如下的原型:
typename std::basic_format_parse_context<CharT>::iterator parse(std::basic_format_parse_context<CharT> pc);
作为函数参数的 pc
对象包含 begin
和 end
两个成员实例函数,分别返回指向 format-spec
字符串头尾的迭代器。parse
函数应该解析 format-spec
,并将解析结果存储在 this
对象中,准备后续的数据格式化操作。parse
函数应该尽可能地解析 format-spec
,并返回一个迭代器指向 format-spec
的末尾表示解析完毕或者返回一个迭代器指向第一个无法被解析的字符。当 format-spec
不能被成功解析时,parse
可以抛出一个 std::format_error
异常表示错误。
STL 为整数、浮点数和字符串实现的 std::formatter
特化模板能够解析的 format-spec
与 python
的 format-spec
一致。有关这些基本类型的 format-spec
的格式信息,请参考 python 的相关文档。
format
实例成员函数需要有如下的原型:
typename std::basic_format_context<OutputIt, CharT>::iterator format(const T& value, std::basic_format_context<OutputIt, CharT> fc);
其中,第一个参数为需要格式化的对象;作为第二个参数的 fc
对象包含一个 out
成员实例函数,该函数返回一个 output iterator 作为格式化操作的输出目标。format
函数应该按照 this
中保存的格式化信息(通过之前的一个 parse
调用解析出),对第一个参数对象进行格式化,并将结果通过 fc.out()
进行输出。输出完毕后,format
应该返回一个迭代器指向输出位置的结尾。
Example
本节将给出一个例子。具体地,我们将定义一个 Fraction
类用于表示一个分数:
struct Fraction { explicit Fraction(int dividend, int divider) noexcept : dividend(dividend), divider(divider) { } int dividend; int divider; }; // struct Fraction
我们将通过特化 std::formatter
类模板的方法,定义 Fraction
类的格式化字符串输出。用户可以选择两种方式输出一个 Fraction
类对象:通过 v
格式限定符输出分数的小数表示,或者通过 f
格式限定符输出分数表示。如果用户没有指定格式限定符,那么应该输出分数表示。用例如下:
Fraction frac { 3, 5 }; std::format("{:v}", frac); // Returns "0.6" std::format("{:f}", frac); // Returns "3/5" std::format("{}", frac); // Returns "3/5"
首先定义 std::formatter
特化类模板:
namespace std { template <> class formatter<Fraction> { public: explicit formatter() noexcept : _fmt(OutputFormat::Fractional) { } typename std::basic_format_parse_context<char>::iterator parse(std::basic_format_parse_context<char> pc); template <typename OutputIt> typename std::basic_format_context<OutputIt, char>::iterator format(const Fraction& value, std::basic_format_context<OutputIt, char> fc) const noexcept; private: enum class OutputFormat { Value, Fractional, }; OutputFormat _fmt; }; // class formatter<Fraction> } // namespace std
然后实现 parse
函数:
template <> typename std::basic_format_parse_context<char>::iterator formatter<Fraction>::parse(std::basic_format_parse_context<char> pc) { if (pc.begin() == pc.end() || *pc.begin() == '}') { _fmt = OutputFormat::Fractional; return pc.end(); } switch (*pc.begin()) { case 'v': _fmt = OutputFormat::Value; break; case 'f': _fmt = OutputFormat::Fractional; break; default: throw std::format_error("Invalid format specification"); } return pc.begin() + 1; }
最后实现 format
函数:
template <> template <typename OutputIt> typename std::basic_format_context<OutputIt, char>::iterator formatter<Fraction>::format( const Fraction& value, std::basic_format_context<OutputIt, char> fc) const noexcept { std::string valueString; switch (_fmt) { case OutputFormat::Value: { auto value = static_cast<double>(value.dividend) / value.divider; valueString = std::format("{}", value); break; } case OutputFormat::Fractional: valueString = std::format("{}/{}", value.dividend, value.divider); break; } auto output = fc.out(); for (auto ch : valueString) { *output++ = ch; } return output; }