之前的文章有朋友反映比较难懂,这篇争取写的简单易懂一些,希望有一点参考价值。
学过 C 语言的同学可能对 printf 都不陌生,也对用 "%d" 这种格式控制符对应于打印一个 int 也不陌生。然而这种打印方式是被 C++ 唾弃的,于是有了复杂的 stream 和可怕的 std::cout << std::internal << std::setw(10) << std::showbase << std::setfill('0') << std::hex << 0x10 << std::endl; 来实现和 printf("%#010x\n", 0x10); 同样的效果。
而 python 似乎提供了一种不错的格式化输出方式:'{:#010x}'.format(0x10) [1]
于是 fmtlib.fmt[2][3] 为 C++ 提供了类似 python 的 format 的实现:fmt::format("{:#010x}", 0x10);
可能有人会问 format("{:#010x}", 0x10); 和 printf("%#010x", 0x10); 有什么区别呢?
实际上区别很大,例如我可以用 format("{}", 16); 的形式直接打印 16 这个 int,而不需要用 "%d" 来明确指定我要打印一个 int。即 format 是通过 template 来实现类型感知的。更进一步说,format 是 类型安全 的。
关于类型安全(type-safe)的话题网上可以搜到很多,我举一个自己碰到的实际例子。
uint32_t value = 468391957727543360UL;
char buf[256];
... // fill the buffer
snprintf(buf+56, sizeof(value), "%x", value);
... // fill the buffer
就是这样一份简单的代码,在后来一次升级的过程中,因业务需要把 uint32_t 改成了 uint64_t,把 "%x" 改成了 "%lx",初始值也改成了一个大值,于是业务发生了问题。因为 buf 最终是通过网络发送给B公司的,即使询问B公司,对方也没有义务把收到的字节反馈回来。花费了很多调试时间,绕了一大圈,通过 wireshark 抓包才得知此处的 value 写到 buf 中时,仅仅被截取了一半。
这其中包含两个问题:
printf 的 格式化控制符 和打印的 值的类型 必须是对应的,因此,当更新代码时,在更新值的类型的时候,需要记得同时也更新对应的格式化控制符。"%x" 为 "%lx",最后实际证明在 64位 Linux 平台中 printf("%lx", value); 是可以正确打印的,但是在 64位 Windows 平台中 printf("%lx", value); 也无法正确打印该值,需要用 printf("%**llx**", value); 才可以正确打印。这可能是因为在 64位 Linux 平台中 unsigned long 是 64 位的,但是在 64位 Windows 平台中,unsigned long 是 32 位的。value 的长度信息,而不是需要具体指出,会让代码更容易维护和减小出错的机会。而 fmt::format 正是解决了这些问题。
fmt::format("{}", value); 返回值类型为 std::string。其中"{}" 可以被看做一个占位符/placeholderfmt::format_to(buf, "{}", value); buf 为 fmt::memory_bufferstd::string s = fmt::format("{} {}", "Hello", "World");
fmt::memory_buffer buf;
format_to(buf, "{}", 42); // replaces itoa(42, buffer, 10)
format 输出格式控制[4]。
fmt::format("{1} {0}", "World", "Hello"); 打印 "Hello World",此处 1 和 0 表示 positional param。"%.." 对应的,在 format 中用 "{:..}" 表示{N:formats},N 为 0,1,2 等自然数;formats 主要由 alignment,sign,width,type 组成,更严谨的定义如下:
d, b, B, o, x, X, n 用法较直观简单,略,可参考[4]x 是不需要加 l 或者 ll 来区分位数的,而是根据变量的类型自动识别。如果是 uint32_t 则打印32位,如果是 uint64_t 则打印64位,于是再也不用操心到底用 %lx 还是 %llx 来打印hex了,从而解决上上文中的问题。c 用法较直观简单,略,可参考[4]s 用法较直观简单,略,可参考[4]e 类似 std::scientific,科学计数法,例如 "1.02e1",即表示 1.02 x 10^1f 类似 std::fixed,定点数,例如 "10.2"g 类似 std::defaultfloat,自动选取 e 和 f 中更短的形式打印a 类似 std::hexfloat,以16进制的形式打印浮点数,一般很少用到。"{:p}", 0xffff 是不可行的,会报 type mismatch,因为 0xffff 是个 int。必须明确对 0xffff 做转换才能打印:"{}", (void*)0xffff。这也是为什么说 fmt::format 是 type-safe 的。"{:p}", ptr 是没必要加 :p 的,因为 {} 会根据 ptr 的类型自动打印成指针,就相当于 "{}", ptr"{:p}", str.c_str() 这个是有必要的,否则将以默认 :s 的格式打印出来<为例:"{:?<6}":其中?可指定为任意字符,取代默认的空格作为 fill/padding。< 左对齐,对于 <>^ 这三个align,其padding都是位于整个字符串最前面/最后面的。对string和numeric类型都适用> 右对齐^ 居中对齐= 这种padding是位于符号+-前缀# 和 输出的数字之间的。only valid for numeric types.0 padding 0, 紧贴在 (width)n 前面。0=。?= 指定了具体其他 padding,则此 0 覆盖之?= 的语法。+ print sign for both pos/neg- print sign only for neg空格 表示前缀要么是空格,要么是-号+号,如果是负数则写出-号n.m 格式6.2f 表示算上正负号,算上小数点,包括 padding,一共长6,小数点后面的位数一定是2。总长不足6补齐6,超过6忽略这个6"." 不是表示精度而是表示截断("{:10.5}","xylophone") 输出 "xylop(后接5个空格)"包括:
#,表示给 :x 加 0x 前缀等,例如 format("{:#04x}", 0); 输出 "0x00"format("{:.{}f}", 3.14, 1); 相当于 format("{:.1f}", 3.14);'{name}'.format(name='World')。需要结合 fmt::arg("name", "World"), 或者 "name"_a=World 来使用。using fmt::literals;fmt::print("{name} {number}", fmt::arg("name", "World"), "number"_a=42));std::string message = "{0}{1}{0}"_format("abra", "cad"); 形式启发于python的.format(),完全identical to fmt::formatformat(L"{}", L'\x42e');, 而不可以 format("{}", L'\x42e');string s0 = format("{} to {}", "a", "b"); // OK: automatic indexing
string s1 = format("{1} to {0}", "a", "b"); // OK: manual indexing
char c = 120;
string s0 = format("{:6}", 42); // s0 == " 42"
string s1 = format("{:6}", 'x'); // s1 == "x "
string s2 = format("{:*<6}", 'x'); // s2 == "x*****"
string s3 = format("{:*>6}", 'x'); // s3 == "*****x"
string s4 = format("{:*^6}", 'x'); // s4 == "**x***"
string s6 = format("{:6d}", c); // s6 == " 120"
string s7 = format("{:=+6d}", c); // s7 == "+ 120" 在中间插入padding
string s7 = format("{:>+6d}", c); // s7 == " +120" 在前面插入padding
string s8 = format("{:?=#6x}", 0xa); // s8 == "0x???a"
string s8 = format("{:?=+#6x}", 0xa);// s8 == "+0x??a"
string s9 = format("{:6}", true); // s9 == "true "
string s1 = format("{0:} {0:+} {0:-} {0: }", -1); // s1 == "-1 -1 -1 -1"
{0: }表示打印参数{0}前缀要么是空格,要么是-号
{0:}表示打印参数{0}前缀要么是-号,要么没有任何前缀
string s1 = format("{0:b} {0:d} {0:o} {0:x}", 42); // s1 == "101010 42 52 2a"
string s2 = format("{0:#x} {0:#X}", 42); // s2 == "0x2a 0X2A"
string s3 = format("{:n}", 1234); // s3 == "1,234" (depends on the locale)