You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 22:52:33