之前的文章有朋友反映比较难懂,这篇争取写的简单易懂一些,希望有一点参考价值。
学过 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^1
f
类似 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)