C++ {fmt}库:如何利用内置格式解析输出自定义有理数类型?
C++ {fmt}库实现有理数类型格式化的问题与解决方案
问题描述
我有一个带分子、分母的模板化有理数类型,正在通过特化fmt::formatter实现格式化。核心需求:
- 复用整数类型的所有格式规范,不用自己写解析代码
- 分母为1时只输出分子,否则输出
分子/分母格式 - 格式字符串指定的宽度要作用于整个输出内容,而非仅分子
目前的代码实现了基础输出,但格式规范只对分子生效,无法让宽度作用于整体,现有代码如下:
template<rational::Rational T> struct fmt::formatter<T> : fmt::formatter<typename T::numerator_type> { using base_type = fmt::formatter<typename T::numerator_type>;; template<class FormatContext> auto format(const T& r, FormatContext& ctx) { auto n = r.num(); auto d = r.den(); if (d == 1) return base_type::format(n, ctx); base_type::format(n, ctx); return format_to(ctx.out(), "/{}", d); } };
解决方案
直接分两次输出分子和分母的方式,会让基类的宽度规范只作用在分子上,所以需要换思路处理:要么先构造完整的输出字符串再应用格式,要么手动利用基类解析的格式参数处理整体宽度。
方法1:构造完整字符串后应用格式(简洁首选)
先把有理数转换成完整的字符串(p/q或p),再让基类的格式化逻辑作用于这个字符串,这样所有格式规范(包括宽度、对齐)都会应用到整体内容上:
#include <fmt/format.h> #include <string> template<rational::Rational T> struct fmt::formatter<T> : fmt::formatter<typename T::numerator_type> { using base_type = fmt::formatter<typename T::numerator_type>; template<class FormatContext> auto format(const T& r, FormatContext& ctx) { const auto n = r.num(); const auto d = r.den(); const std::string output_str = (d == 1) ? fmt::format("{}", n) : fmt::format("{}/{}", n, d); // 让基类处理整个字符串的格式化,自动应用所有解析好的规范 return base_type::format(output_str, ctx); } };
方法2:手动处理宽度与对齐(高效无中间字符串)
如果不想生成中间字符串,可以先计算完整输出的长度,再根据基类解析出的宽度、对齐参数手动处理填充:
#include <fmt/format.h> template<rational::Rational T> struct fmt::formatter<T> : fmt::formatter<typename T::numerator_type> { using base_type = fmt::formatter<typename T::numerator_type>; template<class FormatContext> auto format(const T& r, FormatContext& ctx) { const auto n = r.num(); const auto d = r.den(); const std::string_view fmt_pattern = (d == 1) ? "{}" : "{}/{}"; // 计算未填充时的输出长度 const auto unpadded_length = fmt::format_to_n(nullptr, 0, fmt_pattern, n, d).size; const auto specified_width = this->width; if (specified_width > unpadded_length) { const auto padding_length = specified_width - unpadded_length; // 根据基类解析的对齐方式处理填充 if (this->align == fmt::alignment::left) { // 先输出内容,再补填充字符 fmt::format_to(ctx.out(), fmt_pattern, n, d); return fmt::format_to(ctx.out(), "{:>{}}", "", padding_length); } else { // 先补填充字符,再输出内容 fmt::format_to(ctx.out(), "{:>{}}", "", padding_length); return fmt::format_to(ctx.out(), fmt_pattern, n, d); } } else { // 宽度不足,直接输出完整内容 return fmt::format_to(ctx.out(), fmt_pattern, n, d); } } };
关键注意点
- 继承
fmt::formatter<typename T::numerator_type>后,基类的parse方法会自动处理所有整数格式规范(宽度、对齐、进制、填充字符等),不用自己写解析逻辑 - 方法1更简洁,完全复用基类的格式化能力,不容易出错
- 方法2避免了中间字符串的内存开销,但需要手动处理对齐逻辑,依赖fmt库
formatter的公开成员(比如width、align),兼容性没问题
内容的提问来源于stack exchange,提问作者RandomBits
相关产品推荐
相关产品推荐

