'{' [ \d+ ] [ ':' 格式说明 ] '}'
由各类型自行解析格式说明
对于基本类型和字符串类型, 格式说明基于Python
[ 填充 ( '<' | '>' | '^' ) ] [ '+' | '-' | ' ' ] [ '#' ] [ '0' ] [ 宽度 ] [ 精度 ] [ 'L' ] [ 类型 ]
'{', '}'外任意字符'<': 左对齐'>': 右对齐'^': 居中对齐'+': 在负数和非负数前显示'+'和'-''-': 只在负数前显示'-'' ': 在非负数前显示一个空格'#': 对整数, 在二进制, 八进制, 十六进制前显示"0b", "0B", '0', "0x", "0X", 对浮点数, 结果中总是含小数点'0': 用前导零填充[1-9]\d*: 直接指定宽度'{' [ \d+ ] '}': 用后继一个正整数参数指定宽度'.' ( \d+ | '{' [ \d+ ] '}'): 对整数无效, 对浮点数, 指定小数精度'L': 按照本地环境, 插入适合的分隔字符(','等), 对于布尔值, 用std::numpunct::truename和std::numpunct::falsename输出字符串类型如下:
'b' | 'B': 二进制格式'd' | 'D': 十进制格式(整数默认)'o' | 'O': 八进制格式'x' | 'X': 十六进制格式对于char和wchar_t类型, 则特有
'c': 按字符输出(默认)对于字符串类型, 则特有
's': 按字符串输出(默认)对于bool类型, 则特有
's': 输出"true"或"false"或本地环境形式(默认)对于浮点数, 则特有
'a' | 'A': 十六进制格式'e' | 'E': 科学计数法格式(精度默认为6)'f' | 'F': 固定小数位数格式(精度默认为6)'g' | 'G' 自动浮点数格式(精度默认为6, 默认)对于地址, 则特有
'p': 十六进制格式(自动添加"0x"前缀, 默认)struct MyType {
int value;
};
template<>
struct std::formatter<MyType, char> {
auto parse(std::format_parse_context& parseContext) {
auto symbolsEnd = std::ranges::find(parseContext, '}');
auto symbols = std::string_view(parseContext.begin(), symbolsEnd);
std::cout << "parse(" << symbols << ")" << std::endl;
return symbolsEnd;
}
auto format(MyType const& my, std::format_context& formatContext) {
return std::format_to(formatContext.out(), "MyType({})", my.value);
}
}
对于如下代码
MyType my1{ 123 }, my2{ 234 };
std::cout << std::format("{0:my symbols1}, {1:my symbols2}", my1, my2) << std::endl;
有如下输出
parse(my symbols1)
parse(my symbols2)
MyType(123), MyType(234)
当考虑宽字符(CharT)时, 以下两处的字符类型可以不同
char和wchar_t两种, 但依平台不同, 大小可以为1(char), 2(Windows的wchar_t), 4(Linux的wchar_t)如格式化串类型为wchar_t, 容器类型为char32_t
std::vector<char32_t> wstr;
std::format_to(std::back_inserter(wstr), L"...", ...);
宽字符改造结果如下
template<typename CharT>
struct std::formatter<MyType, CharT> {
auto parse(std::basic_format_parse_context<CharT>& parseContext) {
auto symbolsEnd = std::ranges::find(parseContext, '}');
auto symbols = std::basic_string_view<CharT>(parseContext.begin(), symbolsEnd);
if constexpr (std::is_same_v<CharT, char>) {
std::cout << "parse(" << symbols << ")" << std::endl;
} else if constexpr (std::is_same_v<CharT, wchar_t>) {
std::wcout << L"parse(" << symbols << L")" << std::endl;
}
return symbolsEnd;
}
template<std::output_iterator<CharT const&> It>
auto format(MyType const& t, std::basic_format_context<It, CharT>& formatContext) {
...
}
};
给formatter增加构造和析构函数
template<typename CharT>
struct std::formatter<MyType, CharT> {
formatter() { std::cout << "formatter()" << std::endl; }
~formatter() { std::cout << "~formatter()" << std::endl; }
...
};
然后
print("hello: {:sym1}, {:sym2}\n", my1, my2);
得到输出
formatter()
parse(sym1)
~formatter()
formatter()
parse(sym2)
~formatter()
hello: MyType(123), MyType(234)
每格式化一个对象均会构造和销毁一个formatter.
如果格式说明中没有':'及其后的自定义格式, 那么将不会触发parse的调用.
因此, 有必要在formatter的构造函数中就将其所有变量赋予有效的初始值, 因为parse函数有可能不会被调用
struct MyType {
int value1;
double value2;
};
template<>
struct std::formatter<MyType, char> {
std::formatter<int, char> value1Formatter;
std::formatter<double, char> value2Formatter;
auto parse(std::format_parse_context& parseContext) {
auto symbolsEnd = std::ranges::find(parseContext, '}');
auto symbols = std::string_view(parseContext.begin(), symbolsEnd);
auto symbols1 = symbols.substr(0, symbols.find(','));
auto symbols2 = symbols.substr(symbols.find(',') + 1);
std::format_parse_context value1Format(symbols1);
value1Formatter.parse(value1Format);
std::format_parse_context value2Format(symbols2);
value2Formatter.parse(value2Format);
std::cout << "parse(" << symbols1 << ", " << symbols2 << ")" << std::endl;
return symbolsEnd;
}
auto format(MyType const& t, std::format_context& formatContext) {
auto it = formatContext.out();
std::ranges::copy("MyType("sv, it);
formatContext.advance_to(std::move(it));
it = value1Formatter.format(t.value1, formatContext);
std::ranges::copy(", "sv, it);
formatContext.advance_to(std::move(it));
it = value2Formatter.format(t.value2, formatContext);
std::ranges::copy(")"sv, it);
return it;
}
};
对于以下代码
MyType my1{ 123, 3.14 }, my2{ 234, 6.28 };
std::cout << std::format("hello: {: <6,.4f}, {:06}\n", my1, my2) << std::endl;
输出
parse( <6, .4f)
parse(06, 06)
hello: MyType(123 , 3.1400), MyType(000234, 006.28)
因为在parse中查找边界时是结尾或'}'字符均可, 因此容易实现复用
在formatter的parse和format函数中, 为了输出, 需要从std::format_context::out()中获取一个iterator, 这种获取是移动的.
当使用完iterator后
std::format_context::advance_to()将iterator还回context中std::basic_format_arg::handle()会调用advance_to将iterator还回context
不过在MSVC的当前的iterator的实现中, iterator是可平凡复制的, 因此有没有正确移动iterator对结果并没有什么影响
而且iterator是一个std::back_insert_iterator(++运算符对iterator无效果), 因此前一节"formatter的复用"中format函数也可以简单偷懒实现如下
auto format(MyType const& t, std::format_context& formatContext) {
auto it = formatContext.out();
std::ranges::copy("MyType("sv, it);
value1Formatter.format(t.value1, formatContext);
std::ranges::copy(", "sv, it);
value2Formatter.format(t.value2, formatContext);
std::ranges::copy(")"sv, it);
return it;
}
可以用std::format_to输出到其他迭代器, 比如直接输出到std::cout
std::format_to(std::ostream_iterator<char>(std::cout), "Hello world!");
可以写一个包装函数, 将其包装为print
template<typename... Args>
void print(std::_Fmt_string<Args...> const fmt, Args&&... args) {
std::format_to(std::ostream_iterator<char>(std::cout), fmt, std::forward<Args>(args)...);
}
于是可以
MyType my{ 123 };
print("hello: {}\n", my);
另外std::format_to函数对iterator也是移动的, 需要从其返回值重新接回iterator
auto it = formatContext.out();
it = std::format_to(std::move(it), "my format");
如果不需要格式化输出, 可以用std::ranges::copy
std::ranges::copy("my format", it);
测量输出的大小, 参数同std::format, 大小在返回值获得
size_t size = std::formatted_size("...", ...);
std::vector<char> buf(size);
std::format_to(buf.begin(), "...", ...);
一种比较高效的做法还是事先用数组在栈上分配比较足够的空间, 然后用有计数功能的写定容缓冲的迭代器包装数组, 当format输出内容超出数组容量时再动态分配足够大内存
std::array<char, 256> fixedCapacityBuffer;
fixed_capacity_counter_iterator iter(fixedCapacityBuffer);
std::format_to(iter, "...", ...);
if (iter.count() >= fixedCapacityBuffer.size()) {
std::vector<char> dynamicBuffer();
dynamicBuffer.reserve(iter.count());
std::format_to(std::back_inserter(dynamicBufer), "...", ...);
handle(dynamicBuffer.begin(), dynamicBuffer.end());
} else {
handle(fixedCapacityBuffer.begin(), fixedCapacityBuffer.begin() + iter.count());
}
前一节中使用了内部类std::_Fmt_string, 而不是使用std::string_view, 是因为std::_Fmt_string是consteval, 而std::string_view只是constexpr.
有时候我们需要包装std::format, 而由于std::format只接收consteval的字符串, 并且当前公开的字符串包装类中, 没有一个是consteval的, 字符串通过函数参数传递后只能降级为constexpr, 进而导致consteval的字符串字面量无法直接传给std::format.
std::vformat放宽了格式化串的限制, 可以包装std::vformat来提供类似std::format的功能, 当然, 也会丢失编译期检查格式化串的能力, 下面是print的另一个实现.
template<typename... Args>
void print(std::string_view const fmt, Args&&... args) {
std::vformat_to(std::ostream_iterator<char>(std::cout), fmt, std::make_format_args(std::forward<Args>(args)...));
}
在某些情况下, 一个{}里面可能会需要两个参数, 如
std::format("{:{}}", str, 5);
在parse中可以使用parseContext.next_arg_id(), 获取下一参数的id, 如果已从从格式说明中解析出id, 也可以用parseContext.check_arg_id(id)检查是否越界
然后在format中通过formatContext.arg(id)获取到参数
最后通过std::visit_format_arg访问参数, 例如
template<>
struct std::formatter<MyType, char> {
size_t nextId;
auto parse(std::format_parse_context& parseContext) {
nextId = parseContext.next_arg_id();
return std::ranges::find(parseContext, '}');
}
auto format(MyType const& t, std::format_context& formatContext) {
size_t next;
std::visit_format_arg([&](auto const& arg) {
if constexpr (std::integral<std::decay_t<decltype(arg)>>) {
next = arg;
}
}, formatContext.arg(nextId));
return std::format_to(formatContext.out(), "MyType(nextId={}, next={})", nextId, next);
}
};
对于以下代码
MyType my1, my2;
std::cout << std::format("hello: {:}, {:}\n", my1, 345, my2, 456) << std::endl;
输出
hello: MyType(nextId=1, next=345), MyType(nextId=3, next=456)
注意, 在parse中调用了parseContext.next_arg_id消耗了一个参数, 如果格式化串中使用"{}"来格式化, 那么这个调用不会发生, 进而导致后面format函数中不会取到正确的id
当前MSVC暂不支持对格式化串做编译期检查(正在支持中), 为了支持编译期检查, 尽量给parse和format加上constexpr
如果不需要formatter上下文, 那么可以将parse和format都声明为static.
template<>
struct std::formatter<MyType, char> {
static constexpr auto parse(std::format_parse_context& ctx) {
return std::ranges::find(ctx, '}');
}
static auto format(MyType const& t, std::format_context& formatContext) {
return formatContext.out();
}
};