简单实现 C++ 字符串格式化

最后更新日期:2024-04-28

  字符串格式化是很常见的功能,传统上,我们使用 C 语言的 printf 来格式化。但作为一位 C++ 爱好者,printf 的缺点也很明显:

  1. 非类型安全
  2. 无法添加自定义类型

  std::cout 的问题在于:

  1. 进制和 padding 是通过设置流的全局状态实现的
  2. std::ostringstream 的 str() 方法会复制底层的 buffer ,不够高效
  3. 无法将结果追加到一个现有的字符串上,只能新建字符串再合并,会多一次拷贝

  流行的新一代格式化库如 fmt 的问题在于:

  1. 基于格式串的替换,实现比较繁杂

  市面上其他的 C++ 格式化库也无法让我满意,于是我在自己的 C++ 通用库 xlib 中实现了一个简单的 fmt 模块,使用方法如下:

BasicFormat fmt;

// pad 可实现用 0 补齐
fmt("pi = ", fmt.pad(4, 314), ' '); // => "pi = 0314 "

  可以通过继承来添加自定义类型:

struct Date {
    int year, month, day;
};

struct MyFormat: public BasicFormat {
    using BasicFormat::append;
    static void append(const Date& d, StringPusher& push) {
        append_all(push, d.year, '-', pad(2, d.month), '-', pad(2, d.day));
    }
    X_FMT_IMPLEMENT_FORMAT
};

MyFormat fmt;

fmt(Date { 2012, 4, 1 }); // => "2012-04-01"

  支持自定义类型的方法有这么几种:

  1. 在全局命名空间定义 operator<< ,std::cout 就用的这种
  2. 模板偏特化 + 后期 namespace 写入
  3. 继承

  我一开始用的是模板偏特化的方法,但这种方案的问题是,相关定义是全局的。所以对某一个自定义类型,同一个程序里只能定义一种格式化方法。而且 C++ 的 namespace 是开放的:即后面 include 进来的文件可以往任意 namespace 添加东西,我认为这样太动态。

  我后来改为用继承实现,因为:

  1. 对某一个类型的格式化方法是定义在类上的,同一个程序里可以定义多个不同的类,从而实现对同一类型的多种不同的格式化方法
  2. 类定义好后就不能往里面添加东西了,更有利于程序阅读和分析

附:fmt 的简化实现

class StringPusher {
    std::string& str;
public:
    StringPusher(std::string& str): str(str) {}
    void operator()(char c) {
        str.push_back(c);
    }
    void operator()(std::string_view buf) {
        str.append(buf.data(), buf.size());
    }
};

// 待格式化整数
template<class Int>
struct FormattedInt {
    Int n;
    uint8_t radix; // 进制
    CharCase charCase; // 大小写
    uint8_t padTo; // 补齐位数,0 表示不补齐
};

/**
 * https://stackoverflow.com/questions/27375089/what-is-the-easiest-way-to-print-a-variadic-parameter-pack-using-stdostream
 */
#define X_FMT_IMPLEMENT_FORMAT template<class... Args>\
static void append_all(StringPusher& push, Args&&... args) {\
    using _expander = int[];\
    (void)_expander{ (append(std::forward<Args>(args), push), 0)... };\
}\
template<class... Args>\
std::string operator()(Args&&... args) {\
    std::string res;\
    StringPusher push(res);\
    append_all(push, args...);\
    return res;\
}

struct BasicFormat {
    // 字符串
    static void append(char c, StringPusher& push) {
        push(c);
    }
    static void append(const char* s, StringPusher& push) {
        push(s);
    }
    static void append(std::string_view s, StringPusher& push) {
        push(s);
    }
    static void append(const std::string& s, StringPusher& push) {
        push(s);
    }
    // 整数
    template<class Int, typename std::enable_if_t<std::is_integral_v<Int>>* = nullptr>
    static void append(Int n, StringPusher& push) {
        x::ser::formatInt(n, 10, CharCase::Lower, 0, push); // 省略 formatInt 定义
    }
    template<class Int>
    static FormattedInt<Int> pad(uint8_t n, Int x) {
        return FormattedInt<Int> { x, 10, CharCase::Lower, n };
    }
    template<class Int>
    static void append(FormattedInt<Int> fn, StringPusher& push) {
        x::ser::formatInt(fn.n, fn.radix, fn.charCase, fn.padTo, push); // 省略 formatInt 定义
    }
    X_FMT_IMPLEMENT_FORMAT
};