Boost的C++格式化输出函数库:Format - 小众知识

Boost的C++格式化输出函数库:Format

2015-11-09 08:35:45 苏内容
  标签: Format/Boost/格式化
阅读:2808

不错的文章,原帖:viml.nchc.org.tw/blog/paper_info.php?CLASS_ID=1&SUB_ID=1&PAPER_ID=194

这篇是之前预告过的 Boost C++ Libraries 系列文章的第一篇。所介绍的,是在 Boost 裡用来格式化输出的函式库:boost::format。

他最大的特色是在于它可以使用 C 语言中 printf 的格式化字串,来针对 C++ 的 iostream 做输出、或是产生格式化的字串;相较于 C++ iostream 的 manipulator,boost::format 在使用上更为直觉、简单。而且和 printf 不同的地方在于,他又有 C++ iostream 的 type safe、可以支援自订类别的输出的优点∼

官方网站的介绍可以参考:http://www.boost.org/doc/libs/1_44_0/libs/format/index.html。

C printf 与 C++ iostream
一般在 C 语言的时候,大家应该都很习惯用 printf 这个函式(参考)来做输出的动作。由于 printf 有著强大、简单的格式化输出的能力,所以很多人就算是使用 C++,也会捨弃较为安全的 iostream(参考),而继续使用 printf、fprintf、sprintf 这类的函式来做字串的格式化处理、输出。(话说,Heresy 自己也不完全记得 iostream 要怎麽格式化输出…)

不过实际上,C 语言的 printf 在使用上并不是非常地安全。最主要的问题,在于使用 printf 的时候,并不是 type safe 的!一个很简单的例子就是:

char*   x = "abcd";
printf( "%d", x );
由于在使用 printf 输出的时候,需要先指定输出资料的型别(%d、%f 等),所以其实一不小心就有可能弄错,变成像上面一样,指定了错误的输出型态。另外,也由于 printf 设计上的问题,所以如果要输出自定义的类别资料,会变得相对麻烦。

而如果是要用 sprintf 这个函式来产生格式化的字串的话,更有可能产生记忆体使用上的问题,Heresy 之前也有写过一篇《用 snprintf / asprintf 取代不安全的 sprintf》,就是在讲这部分的东西,有兴趣的人可以参考看看。

基本上 C++ 的 iostream 就已经解决这些问题了。如果是使用 C++ 的 iostream 的话,其实在各方面的问题相对都少很多,用起来也相对简单很多;对于自订类别的输出,更可以用 operator overloading 的方法,来为每一个类别都写一个属于自己、并且符合 iostream 使用方法的输出。

但是如果提到格式化输出的部份的话,虽然 C++ 的 iostream 有提供「manipulator」来让使用者针对输出的格式做控制(参考),但是包含 Heresy 自己本身在内,Heresy 知道有在写 C++ 程式的人,好像大多都还是习惯使用 printf 系列的函式来做,而不会使用 iostream 的 manipulator。毕竟,在 Heresy 看来,他既不好记、也不好用啊…

 

boost::format 基本使用
而 Boost 的 Format 这个函式库(官方介绍),基本上就是为了让程式设计师可以更简单地使用 C++ 的 iostream 来进行格式化输出而开发的!如同 Heresy 在一开始就提过的,boost::format 提供了一个和 C 的 printf 类似的格式化字串(format string)的语法定义,来让程式开发者可以非常简单地做到和 printf 一样效果的格式化输出∼而同时,他也保有了 C++ 的 iostraem 的各项优势,对于要做格式化输出的 C++ 程式开发人员来说,boost::format 应该是个相当好用、也值得一试的的函式库!

boost::format 是一个 header-only 的函式库,只要准备好 header 档,不用预先编译就可以使用了,在使用上相当地便利。而在这个函式库裡,主要是提供了一个 format 的类别(注一),来让程式开发者来做操作。下面是一个简单的例子:

#include <stdlib.h>
#include <iostream>
#include <boost/format.hpp>

using namespace std;

int main( ) {  cout << boost::format( "%2.3f, %d" ) % 1.23456 % 12 << endl; }
黄底的部分,就是 boost::format 相关的程式了。首先,要使用 boost::format,我们必须要先 include「boost/format.hpp」这个档案;只要 include 了这个档案后,就可以使用 boost::format 的功能了。

而 boost::format 最接近 printf 的用法,也就是上面这样的形式(POSIX-printf style)了∼这样的写法在透过 cout 做输出后的结果,会和

printf( "%2.3f, %d", 1.23456, 12 );
完全一样。

实际上,这边是使用「"%2.3f, %d"」这个格式化字串,来建立一个 boost::format 的物件,并透过这个物件来做之后格式化的操作;而这边所使用的格式化字串,和使用 printf 时是完全相同的。

而除了上面这种「Posix-Printf style 」以外,也还有所谓的「simple style」(简单风格)的用法可以使用,下面就是一个简单的例子:

cout << boost::format( "%1%, %2%" ) % 1.23456 % 12 << endl;
在这种风格的写法中,是在格式化字串裡,用「%1%」来代表之后的第一个变数、用「%2%」来代表第二个变数;透过这样的定义,我们就可以自行调整变数的顺序、同时也可以重複地使用某一项变数了∼例如:

cout << boost::format( "%1%, %2%, %1%" ) % 1.23456 % 12 << endl;
这样写的话,输出的结果就会是「1.23456, 12, 1.23456」。 不过由于这个写法没有特别指定格式化的设定,所以所有变数都会用预设的方法做输出。

 

boost::format 物件的操作
前面已经有提到,boost::format 实际上是一个类别,在使用时实际上会产生一个型别是 boost::format 的物件,来进行后续的操作;之后所有的变数,都是透过呼叫 operator% 的方式,依序传给这个物件(注二),最后再透过 operator<< 把他的资料输出传给 cout。

相较于此,printf 本身是一个参数数目可变(variable-length argument)的函示,所以所有要输出的变数,都是用逗号隔开、以函式引数的方式传进去的。所以这两者虽然在程式的写法上看起来很类似,但是在概念和实作方法上,是完全不同的。

像下面这个例子:

cout << boost::format( "%1%, %2%" ) % 1.23456 % 12 << endl;
实际上可以看成:

cout << ( ( boost::format( "%1%, %2%" ) % 1.23456 ) % 12 ) << endl;
而也由于 boost::format 实际上是以物件的形式在运作的,所以实际的执行过程,就相当于:

boost::format fmt( "%1%, %2%" );
fmt % 1.23456;
fmt % 12;
cout << fmt << endl;
这也代表程式开发者可以把 boost::format 这个物件记录下来,重複地使用∼例如下面就是一个重複使用 boost::format 物件的例子:


boost::format fmt( "Test:< %1.2f, %1.2f >" );
cout << ( fmt % 1.234 % 123.1 ) << endl;
cout << fmt % 5.678 % 1 << endl;
不过要注意的是,透过 operator% 传给 boost::format 物件(fmt)的变数是会储存在物件内部的,所以可以分批的传入变数;但是如果变数的数量不符合的话,在编译阶段虽然不会出现错误,可是到了执行阶段还是会让程式当掉,所以在使用上必须小心一点。不过,在有输出后,是可以再重新传入新的变数、重複使用同一个 boost::format 物件的。

 

透过 boost::format 产生字串
前面提的方法,都是把 boost::format 产生的结果直接输出到 ostream 的用法,那如果是要把格式化输出的结果产生成字串继续使用呢?很简单,因为 boost 已经有提供对应的函式可以做这件事了∼基本上有两种方法,第一个方法是用 boost::str() 这个函式:

string tmp = boost::str( boost::format("<%1%>") % "Hi!" );
另一个方法则是用 boost::format::str() 这个函式:

boost::format fmt("<%1%>"); fmt % "Hi!"; string tmp = fmt.str();
或是:


string tmp = ( boost::format("<%1%>") % "Hi!" ).str();
这两者基本上是一样的,只是程式的写法不同罢了。

 

语法细节
前面大概提到了所谓的 POSIX printf style 和 simple style 两种用法。实际上 boost::format 所使用的格式化字串的语法,是依照 Unix98 Open-group printf,再做一些延伸而定的;它的形式是:

%[N$][flags][width][.precision]type-char
其中大部分的内容都和传统的 printf 相同(参考),只有部分不一样。(注四)

像是在 flags 的部分,boost::format 除了本来的「-」是向左对齐外,还多了新的置中对齐的「=」、以及内部对齐(internal alignment)的「_」,这两者就是 printf 没有的。而除了可以用「%%」来输出「%」符号外,也多了可以用「%nt」来填入 n 个空格、或是用「%|nTX|」来填入 n 个 X(X 为单一字元)的功能。

此外,也有某些东西的行为会和 printf 不太一样,不过由于比较细节,在这边就不赘述了;有兴趣的请自行参考《Differences of behaviour vs printf》的部分。

而在实际使用上,大致应该分成下面几种形式:

%N%:(Simple style)最简单、没有格式化的简化写法,其中 N 只是单纯标记是第几个变数。
%spec:(POSIX-printf style 格式化字串)这部分主要是相容于 printf 的写法,基本上可以把本来用在 printf 上的写法直接拿来用。当然,spec 的部分也有支援 boost::format 额外定义的新东西可以用。
%|spec|:这是用「|」来做分隔的表示方法。spec 基本上和前者是相同的,这种写法主要的优点是可以省略指定型别的字元(printf 裡的「type-conversion character」),同时也可以加强程式码的可读性。
例如:「%|-5|」就是代表向左对齐、宽度是 5,根据变数型别的不同,和「%-5g」、「%-5f」等是等价的。

其中,看起来比较特别的写法,或许是「%|1$+4.2|」这样的形式吧∼它代表的意义基本上就是把第一个变数(1$),以「+4.2」的形式来做输出;而这边也没有特别指定输出的型别,所以在执行阶段的行为,可能会根据传入的变数的型别而有所不同。
 

范例
由于 boost::format 的应用变化非常地多,所以Heresy 在这边不打算针对 boost::format 举出太多的范例,基本上只由官方的范例裡,挑一组 Heresy 觉得有代表性的出来,下面就是程式码:

cout << boost::format("(x,y) = (%+5d,%+5d) ") % -23 % 35;
cout << boost::format("(x,y) = (%|+5|,%|+5|) ") % -23 % 35;

cout << boost::format("(x,y) = (%1$+5d,%2$+5d) ") % -23 % 35;
cout << boost::format("(x,y) = (%|1$+5|,%|2$+5|) ") % -23 % 35;
这四行不同的 boost::format 写法输出的结果都是一样的,会是:

(x,y) = (  -23,  +35)
这组例子也大致上代表 boost::format 几种不同形式的写法了∼有兴趣的话,稍微研究一下,应该可以简单地找到这些写法间的差异了。

 

效能问题
boost::format 虽然在使用上算是满方便的,但是实际上在效能面来说,并不是非常地好,这点在官方的网页就有特别提出来。基本上,在一般状态下,使用 printf 的效能会是最好的,iostream 会比 printf 慢一些,而 boost::format 则由于又有其他的 overhead,所以又会更慢。官方也有提供一些测试数据,如果在 release 模式下,iostream 的操作所需的时间大约会是 printf 的 1.6 倍;而 boost::format 所需的时间则会是 iostream 的 2 ~ 3 倍左右,也就是大约是 printf 的 3 ~ 5 倍。

从这个测试数据应该就可以发现,其实 boost::format 的效能并不好。所以如果程式本身的效能瓶颈是在这类的字串输出、处理的话,那使用 boost::format 可能就不是一个好的选择,因为他确实有可能会让效能变差;所以在这种情况下,最好的方法应该还是回去用 printf 了∼

不过实际上,一般的程式主要的效能瓶颈应该不会是在这一部分,所以在这种状况下使用 boost::format 的话,应该不会对整体效能造成很大的影响;相对地,却有可能因为使用 boost::format 而减少不少开发时的时间成本。所以如果是在这种状况下的话,boost::format 应该还是有相当的实用性的。

 

结语
对于 boost::format 的介绍,大概就到这先告一个段落了。其实,讲的应该不算是很完整,有不少细节都被 Heresy 跳过了,只是一个简单地介绍罢了。真正要完全学会的话,可能还是得回官方网站看看了;相信如果愿意花时间的话,应该可以挖到更多进阶的用法才对!

另外,由于 boost::format 的语法定义主要是基于 printf 的语法来做沿伸的,所以 Heresy 在这边也就决定跳过了不少相关的说明;但是实际上在写这篇文章的过程中,却也发现其实 printf 的格式化字串的语法,有不少是 Heresy 之前也没注意到的、没搞懂的…或许,之后还是要再仔细研究看看吧…

 

附注
实作上是一个 template 的 class:basic_format。
boost::format 的 operator% 定义方法其实和 ostream 的 operator<< 很类似,它的形式是「format& format::operator%(const T& x)」,会回传一个 format 的参考,所以可以一直用 operator% 串下去。
对于使用者自订的型别,只要有定义 operator<<,让他可以透过 iostream 输出,就可以用在 boost::foramt 上。
Visual C++ 的 printf 似乎不支援格式化字串的「N$」(positional format specification),不过在 gcc 上应该是可以用的。

扩展阅读
相关阅读
© CopyRight 2010-2021, PREDREAM.ORG, Inc.All Rights Reserved. 京ICP备13045924号-1