C++:在fmt库中重载内置double类型格式化器
如何扩展fmt库的double格式化器以支持单位类型
这个问题我之前也遇到过,fmt库对基本类型的格式化器做了特殊处理,直接特化fmt::formatter<double>确实不会覆盖默认实现——原因很简单:fmt库在你的代码之前就已经实例化了默认的formatter<double>特化,编译器会优先选用已经存在的实例,而不是你后来定义的特化。你看到的Foo类型能触发自定义格式化,是因为它继承的是你自己定义的formatter<double>特化,和fmt内置的默认实现完全是两个东西。
下面给出两种可行的解决方案,既满足你“自定义格式说明符完成单位转换”的需求,又符合fmt库的设计规范:
方案1:使用包装类(推荐)
创建一个包装类来包裹double值和对应的单位类型,然后为这个包装类特化fmt::formatter,这样就能解析格式字符串中的[angle]这类标记,完成单位转换后再委托给默认的double格式化器。
#include <fmt/format.h> #include <cmath> #include <string> // 定义单位枚举 enum class Unit { Angle, Length }; // 包装类:存储原始值和单位 struct UnitValue { double value; Unit unit; }; // 为UnitValue特化格式化器 template <> struct fmt::formatter<UnitValue> { // 存储标准的double格式说明(比如.3f)和解析出的单位 std::string base_format; Unit target_unit; // 解析格式字符串:提取标准格式和单位标记 constexpr auto parse(format_parse_context& ctx) -> decltype(ctx.begin()) { auto it = ctx.begin(); // 先读取标准的double格式说明,直到遇到[ while (it != ctx.end() && *it != '[') { base_format += *it; ++it; } // 解析单位标记(如果存在) if (it != ctx.end() && *it == '[') { ++it; // 跳过[ std::string unit_str; while (it != ctx.end() && *it != ']') { unit_str += *it; ++it; } if (it != ctx.end()) ++it; // 跳过] // 将字符串转换为Unit枚举 if (unit_str == "angle") target_unit = Unit::Angle; else if (unit_str == "length") target_unit = Unit::Length; // 可扩展更多单位的解析逻辑 } return it; } // 执行格式化:先转换单位,再用标准格式输出 template <typename FormatContext> auto format(const UnitValue& uv, FormatContext& ctx) -> decltype(ctx.out()) { double converted_value = uv.value; // 根据单位执行转换逻辑(示例:弧度转角度、米转千米) if (uv.unit == Unit::Angle && target_unit == Unit::Angle) { converted_value = uv.value * 180.0 / M_PI; } else if (uv.unit == Unit::Length && target_unit == Unit::Length) { converted_value = uv.value / 1000.0; } // 委托给默认的double格式化器,使用解析好的标准格式 return fmt::format_to(ctx.out(), "{:" + base_format + "}", converted_value); } }; int main() { // 示例:存储的是弧度,要以角度格式输出 double rad_angle = M_PI / 4; fmt::print("angle = {:.3f[angle]}\n", UnitValue{rad_angle, Unit::Angle}); // 示例:存储的是米,要以千米格式输出 double meter_length = 2500.0; fmt::print("length = {:.2f[length]}\n", UnitValue{meter_length, Unit::Length}); return 0; }
这个方案的优势是:
- 完全不影响原有
double类型的格式化逻辑 - 格式字符串的写法和你期望的
{:.3f[angle]}完全一致 - 单位转换逻辑清晰,易于扩展
方案2:使用包装函数(更简洁)
如果你觉得包装类有点繁琐,可以用模板包装函数来标记单位,然后为包装后的类型特化格式化器,这样不需要在格式字符串里写单位标记:
#include <fmt/format.h> #include <cmath> enum class Unit { Angle, Length }; // 模板包装类:通过模板参数标记单位 template <Unit U> struct TaggedDouble { double value; explicit TaggedDouble(double v) : value(v) {} }; // 简化的包装函数,方便调用 constexpr auto as_angle(double v) { return TaggedDouble<Unit::Angle>(v); } constexpr auto as_length(double v) { return TaggedDouble<Unit::Length>(v); } // 为TaggedDouble特化格式化器,继承默认的double格式化器 template <Unit U> struct fmt::formatter<TaggedDouble<U>> : fmt::formatter<double> { template <typename FormatContext> auto format(const TaggedDouble<U>& td, FormatContext& ctx) -> decltype(ctx.out()) { double converted = td.value; // 编译期判断单位,执行转换 if constexpr (U == Unit::Angle) { converted = td.value * 180.0 / M_PI; } else if constexpr (U == Unit::Length) { converted = td.value / 1000.0; } // 委托给默认的double格式化器 return fmt::formatter<double>::format(converted, ctx); } }; int main() { double rad_angle = M_PI / 4; fmt::print("angle = {:.3f}\n", as_angle(rad_angle)); double meter_length = 2500.0; fmt::print("length = {:.2f}\n", as_length(meter_length)); return 0; }
这个方案的优势是代码更简洁,调用时语义明确,适合不需要动态修改单位的场景。
为什么直接特化fmt::formatter<double>不生效?
fmt库为了优化性能,在内部提前实例化了基本类型(比如double)的格式化器。当你在自己的代码中特化fmt::formatter<double>时,编译器已经看到了fmt库提供的默认特化实例,因此会优先选用默认实现,而忽略你的特化。这是C++模板特化的规则决定的,不是fmt库的“限制”,而是合理的设计。
内容的提问来源于stack exchange,提问作者TD22057
相关产品推荐
相关产品推荐

