Output Stream Type Preservation
Posted on May 4th, 2009 at 1:05 by fr3@K

上週看到了一篇引起我興趣的 blog - C++ Format String using boost::format. 冒著再次被人指為 屁的半死 的風險, 身為男子漢的我, 還是決定把心中的 替代解法 寫給了 Hubert 參考. Hubert 建議用 Boost.Format 的解法很好, 可我就是就是忍不住貪玩啊.

緣由交代完了. 有興趣看給 Hubert 參考的解法的朋友請自便, 因為接下來要談的不是它1, 而是另一個沒那麼漂亮, 但卻挺有趣的解法.


相信很多人都有這樣的需求 - 把 object 轉成 human readable string, 或許經過簡單的 formatting. 我一開始想到的就是:

    Listing 1

    C++:
    1. // Assuming `foo` is an integer.
    2. ostringstream oss;
    3. oss <<"The value of foo is: " <<foo;
    4. string output = oss.str();

雖然這完全是能達到我的目的, 但也正如 Hubert 說的:

總覺得透過 operator += 以及 stringstream 等東西,並沒有辦法像 sprintf 那樣直覺而優雅的將變數置換至字串當中。

那把對 oss 做 insertion 的 code 跟對其呼叫 str() member function 的 code 寫在一起會不會稍微優雅些? 把 Listing 1 的三個 statement 縮減成兩個:

    Listing 2

    C++:
    1. ostringstream oss;
    2. string output =
    3.   (oss <<"The value of foo is: " <<foo).str();

的確就稍微好那麼一些些 ><. 如果這段 code snip 可以 compile 的話.

ostringstream 的 instance (actually lvalue) 做 insertion 呼叫到的實際上是它的 base class - ostream 的 inserter. 這些 inserter 的 return value 當然也是 ostream&. 換句話說, 對 ostream 的 derived class 的 instance 做 insertion, 在沒有特殊的 overload 的狀況下, return value 都不會是 derived class 的 reference. 也因此無法對這些 return value 呼叫 derived class 的特異功能; 例如 ostringstream::str() - 把 insert 到 ostringstream 的 buffer 的內容轉成 string 傳回來.

在我看來, Listing 2 雖不合法, 但會是好用的語法, 可以把 code 寫的更乾淨利落. 於是我考慮了下面的 inserter template:

    Listing 3

    C++:
    1. template <class OutputStream, class T>
    2. OutputStream& operator<<(OutputStream& os, const T& x)
    3. {
    4.   ostream& base = os;
    5.   base <<x;
    6.   return os;
    7. }

由於要能夠給各種繼承自 ostream 的 output stream 使用, 因此第一個參數 - OutputStream - 的 type 得是個 template parameter, 才能讓 compiler 認為這個 template 是比 base class (ostream) 的 inserter 更好的選擇. 但也因此引發兩個使它無法被使用的 side effect:

  1. 這個 template 會影響所有 user-defined type 的 operator<<, 原本沒問題的 code 在有了這個 template 後可能無法 compile 或正常運作.
  2. 當 template parameter OutputStream 已是 ostream 時, 這個 template inserter (inserter template 實例化所產生的實體) 會沒完沒了的 recursive 自己 call 自己, 會在 run time 發作直到把 stack 吃光為止.

也考慮過換個 operator 來實現這樣的功能, 例如 operator%, 但那依然無法解決上述的第一個 side effect, 只是換個 operator 出問題. 於是還是請出了 Boost:

    Listing 4

    C++:
    1. template <class OutputStream, class T>
    2. typename boost::disable_if<
    3.   boost::mpl::or_<
    4.     boost::mpl::not_<
    5.       boost::is_base_of<ostream, OutputStream>>,
    6.     boost::is_same<OutputStream, ostream>>,
    7.   OutputStream>::type& operator<<(
    8.     OutputStream& os, const T& x)
    9. {
    10.   ostream& base = os;
    11.   base <<x;
    12.   return os;
    13. }

Note: 由於不明原因, 我用的 code highlighter plugin 會把某些空格省略掉. 這通常不會造成明顯的問題, 結果依然是合法的 code. 但在 Listing 4 的 line 5 and 6 的結尾, 這個 plugin 把應該是 > > (中間有 space) 的 code 顯示成在現行標準下不合法的連續兩個 >. 請特別注意.

宣告這個 inserter template 的 Listing 4 可以這樣解讀; 不讓 compiler 考慮 (boost::disable_if) 採用這個 template (同時也不會有具體實現這個 template 的問題), 如果下列某個條件成立 (boost::or_):

  1. ostream 不是 (boost::not_) OutputStream 的 base class (boost::is_base_of).2
  2. OutputStream 就是 (boost::is_same) ostream.

這段 code 既繞舌又不好寫. 連屁的半死的我也寫了超過十分鐘才寫對. 不過結果看起來不錯, 保留了output stream 做 insertion 之前的型別, 也在沒有明顯的 side effect 的情況之下讓類似 Listing 2 的操作合法了.

Full source code: ostream_type_preserve.cpp

  1. 下一篇文字 才要進一步談寫給 Hubert 參考的解法, 以及在 C++0x 的標準下優化的方法. []
  2. 如果 boost::is_base_of 的兩個 template parameter 相同時 - e.g. boost::is_base_of<X, X> - result 為真. []
del.icio.us:Output Stream Type Preservation digg:Output Stream Type Preservation spurl:Output Stream Type Preservation newsvine:Output Stream Type Preservation furl:Output Stream Type Preservation Y!:Output Stream Type Preservation 黑米共享書籤:Output Stream Type Preservation 推推王:Output Stream Type Preservation
Previous Post
« Contemplation of Prejudice «
Next Post
» NoScript Gone Bad!? WTF! »

3 Comments »

Comment #5979

我自己是用 boost::format 加上一”些” wrappers. 類似這樣:

template <class T1>
inline std::string MyFormat(const char *Format, const T1& t1)
{
   return ( boost::format(Format) % t1 ).str();
}

template <class T1, class T2>
inline std::string MyFormat(const char *Format, const T1& t1, const T2& t2)
{
   return ( boost::format(Format) % t1 % t2 ).str();
}

就這樣一大堆 overloaded function 來對應不同數量的參數,好處是不需要用 % 這個符號,用起來比較符合原來的習慣:

cout << MyFormat("The value of foo is: %d", foo)  << endl;

ed: code snips formatted - fr3@K

Comment by av — May 4, 2009 @ 22:38


Comment #5981

再補充一下,用這種方法,我弄了從 0 個參數到 20 個參數的版本,所以你可以寫
cout << MyFormat(”no value” ) << endl;
cout << MyFormat(”The value of foo is: %d”, foo) << endl;
cout << MyFormat(”x = %d, y = %f z = 0x%x: %d”, 1, 2.5, 0xff) << endl;
cout << MyFormat(”%s say: %s because he has only %d dollors”, “simon”, str, szMoney ) << endl;

20 個參數應該很夠用了.

Comment by av — May 4, 2009 @ 22:49


Comment #5984

Hi av,

你的方法很實用, 最明顯的優點就是保留了 printf 的 format 方式與 operator<< 的習慣.

我想是文內沒清楚表達, 這篇 post 想呈現重點的其實是在 insertion 時保留 output stream 的型別. 這樣可能可以有一些不只是字串化與格式化的有趣應用. 雖然這個 idea 是從 formatting 那邊發想來的.

譬如… 假設在操作一個繼承自 iostream 的 TCP stream, 就能夠在 insert (send) 一個 request 後, 直接 extract (recv) response:

  request req(...);
  response rsp;
  tcpstream << req >> rsp;

或許該把這個我認為有趣且可能有用的例子加到 post 裏面.

Comment by fr3@K — May 5, 2009 @ 0:09


Comments RSS TrackBack URI

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>