Boost 的 C++ 格式化輸出函式庫:Format

| | 5 Comments| 17:32
Categories:

這篇是之前預告過的 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》的部分。

而在實際使用上,大致應該分成下面幾種形式:

  1. %N%:(Simple style)最簡單、沒有格式化的簡化寫法,其中 N 只是單純標記是第幾個變數。
  2. %spec:(POSIX-printf style 格式化字串)這部分主要是相容於 printf 的寫法,基本上可以把本來用在 printf 上的寫法直接拿來用。當然,spec 的部分也有支援 boost::format 額外定義的新東西可以用。
  3. %|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 之前也沒注意到的、沒搞懂的…或許,之後還是要再仔細研究看看吧…

附註

  1. 實作上是一個 template 的 class:basic_format。
  2. boost::format 的 operator% 定義方法其實和 ostream 的 operator<< 很類似,它的形式是「format& format::operator%(const T& x)」,會回傳一個 format 的參考,所以可以一直用 operator% 串下去。
  3. 對於使用者自訂的型別,只要有定義 operator<<,讓他可以透過 iostream 輸出,就可以用在 boost::foramt 上。
  4. Visual C 的 printf 似乎不支援格式化字串的「N$」(positional format specification),不過在 gcc 上應該是可以用的。

5 thoughts on “Boost 的 C++ 格式化輸出函式庫:Format”

  1. “當傳入的數目超過所需的數目後,他會重新開始;”我測試的結果是會丟出如下的例外:what(): boost::too_many_args: format-string referred to less arguments than were passed不知道您說的會重新開始是什麼意思?

  2. 您好,感謝您的指證。這部分的確是 Heresy 弄錯了,過多的參數的確是會造成執行階段的問題;而要可以重新傳入參數,則是要在輸出後才可以。文章內容已經做了對應的修改。

發佈回覆給「狗王」的留言 取消回覆

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *