最后更新日期:2024-04-28
字符串格式化是很常见的功能,传统上,我们使用 C 语言的 printf 来格式化。但作为一位 C++ 爱好者,printf 的缺点也很明显:
- 非类型安全
- 无法添加自定义类型
std::cout 的问题在于:
- 进制和 padding 是通过设置流的全局状态实现的
- std::ostringstream 的 str() 方法会复制底层的 buffer ,不够高效
- 无法将结果追加到一个现有的字符串上,只能新建字符串再合并,会多一次拷贝
流行的新一代格式化库如 fmt 的问题在于:
- 基于格式串的替换,实现比较繁杂
市面上其他的 C++ 格式化库也无法让我满意,于是我在自己的 C++ 通用库 xlib 中实现了一个简单的 fmt 模块,使用方法如下:
;
BasicFormat fmt
// pad 可实现用 0 补齐
("pi = ", fmt.pad(4, 314), ' '); // => "pi = 0314 " fmt
可以通过继承来添加自定义类型:
struct Date {
int year, month, day;
};
struct MyFormat: public BasicFormat {
using BasicFormat::append;
static void append(const Date& d, StringPusher& push) {
(push, d.year, '-', pad(2, d.month), '-', pad(2, d.day));
append_all}
X_FMT_IMPLEMENT_FORMAT};
;
MyFormat fmt
(Date { 2012, 4, 1 }); // => "2012-04-01" fmt
支持自定义类型的方法有这么几种:
- 在全局命名空间定义
operator<<
,std::cout 就用的这种 - 模板偏特化 + 后期 namespace 写入
- 继承
我一开始用的是模板偏特化的方法,但这种方案的问题是,相关定义是全局的。所以对某一个自定义类型,同一个程序里只能定义一种格式化方法。而且 C++ 的 namespace 是开放的:即后面 include 进来的文件可以往任意 namespace 添加东西,我认为这样太动态。
我后来改为用继承实现,因为:
- 对某一个类型的格式化方法是定义在类上的,同一个程序里可以定义多个不同的类,从而实现对同一类型的多种不同的格式化方法
- 类定义好后就不能往里面添加东西了,更有利于程序阅读和分析
附:fmt 的简化实现
class StringPusher {
std::string& str;
public:
(std::string& str): str(str) {}
StringPushervoid operator()(char c) {
.push_back(c);
str}
void operator()(std::string_view buf) {
.append(buf.data(), buf.size());
str}
};
// 待格式化整数
template<class Int>
struct FormattedInt {
;
Int nuint8_t radix; // 进制
; // 大小写
CharCase charCaseuint8_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) {
(c);
push}
static void append(const char* s, StringPusher& push) {
(s);
push}
static void append(std::string_view s, StringPusher& push) {
(s);
push}
static void append(const std::string& s, StringPusher& push) {
(s);
push}
// 整数
template<class Int, typename std::enable_if_t<std::is_integral_v<Int>>* = nullptr>
static void append(Int n, StringPusher& push) {
::ser::formatInt(n, 10, CharCase::Lower, 0, push); // 省略 formatInt 定义
x}
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) {
::ser::formatInt(fn.n, fn.radix, fn.charCase, fn.padTo, push); // 省略 formatInt 定义
x}
X_FMT_IMPLEMENT_FORMAT};