這一篇是來大概介紹一下 Boost C Libraries 裡的 lexical_cast 這個函式庫(官網);他的功能相當簡單,主要就是提供了一個 template 函式的介面,來做到任一型別和文字間的轉換。
很多時候,在寫程式的時候,我們會需要把字串文字(例如 std::string)的資料轉成數值(例如:double),或是反過來,把數值資料轉成文字;尤其是當要從檔案讀取資料,或是要求使用者輸入時,基本上這都是會常常要用到的功能。
雖然在標準的 C 或 C 裡的確有提供部分的函式、例如 atoi()(參考),可以快速地做到部分的轉換,但是其實都有相當的限制。例如基本上他們只針對內建的部分型別有提供對應的函式(應該是只有 int、long、double),而且也沒有反向的轉換(itoa() 並非標準函式,要用標準的寫法要用 sprintf(),參考);而另外由於不同的型別就是不同的函示、也很難寫出統一個方法,來針對轉換的型別做擴充。
使用 C IO Stream
而在標準 C 的函式庫裡,實際上他有透過 IO Stream 的概念(參考),提供了 stringstream 這個類別(參考),來進行可自訂的、和文字間的轉換。像下面就是一個簡單的例子:
#include <stdlib.h>
#include <string>
#include <sstream>
#include <iostream>
using namespace std;
int main( int argc, char** argv )
{
// string to float
string sTmp = "123.456";
stringstream mSS( sTmp );
float fTmp;
mSS >> fTmp;
cout << "Convert from string to float: " << fTmp << endl;
// float to string
fTmp = 1234.567f;
stringstream mSS2;
mSS2 << fTmp;
sTmp = mSS2.str();
cout << "Convert from float to string: " << sTmp << endl;
}
第一段的程式碼,基本上就是將 sTmp 這個字串,透過 stringstream 的 operator>> 來轉換為浮點數 fTmp;而第二段程式碼,則是反過來,把 fTmp 這個浮點數,轉換成字串、儲存到 sTmp 這個字串裡。基本上,這邊主要就是透過 stringstream 這個「字串流」來做;而他基本的用法,其實和標準輸入、輸出的 cout / cin 是一樣的∼差別只在於,他的資料來源和輸出結果,都是字串而已。
而這邊用的範例是比較簡單,直接用內建的 float、string 這兩種型別來作範例;而如果是要擴充到其他的自訂型別的話,只要去定義各種型別的 operator<< 和 operator>> 就可以了∼下面是一個簡單的範例:
class CVector2
{
public:
float x;
float y;
};
istream& operator>>( istream& iS, CVector2& v )
{
iS >> v.x >> v.y;
return iS;
}
ostream& operator<<( ostream& oS, const CVector2& v )
{
oS << v.x << "/" << v.y;
return oS;
}
這個範例裡定義了一個 2D 的向量 CVector2、裡面有 x、y 兩個 float 的成員變數;另外,也定義了 operator<< 和 operator>> 這兩個全域函式,可以用來處理 CVector2 這個自訂型別與 STL stream 間的運作。例如下面就是簡單的範例:
string sTmp = "12 34";
stringstream mSS( sTmp );
CVector2 v;
mSS >> v;
cout << v << endl;
也就是在針對 CVector2 寫好 operator<< 和 operator>> 後,他就擁有標準的 C IO stream 的處理能力了!這點算是相當方便的∼
boost::lexical_cast
不過,如果要直接使用 stringstream 來做的話,其實在操作上會比較繁瑣,所以 Boost C Libraries 就提供了所謂的「lexical_cast」,專門來處理這些各種型別和字串間的轉換。
在使用 lexical_cast 時,必須要先加入「#include <boost/lexical_cast.hpp>」;而它的使用基本上非常簡單,就是一個 template 函式,他的形式是:
template<typename Target, typename Source>
Target lexical_cast(const Source &arg);
而使用上,如果直接修改這篇文章第一個使用 stringstream 的程式的話,就會變成下面的形式。
// string to float
string sTmp = "123.456";
float fTmp = boost::lexical_cast<float>( sTmp );
cout << "Convert from string to float: " << fTmp << endl;
// float to string
fTmp = 1234.567f;
sTmp = boost::lexical_cast<string>( fTmp );
cout << "Convert from float to string: " << sTmp << endl;
基本上,只要把要轉換的資料當參數傳進去,在指定要轉換的目標型別,就可以了!他省去了所有對於 stringstream 的操作,而直接用一個統一的函式來做,在許多時候會是相當方便的∼
而如果要讓自訂的型別也可以適用於 lexical_cast 的話,來源的型別(Source)和目標的型別(Target)分別需要符合一些條件:
- 來源型別必須要是「OutputStreamable」,也就是有定義 std::ostream 或 std::wostream 的 operator<<。
- 目標型別必須要是「InputStreamable」,也就是要有針對 std::istream 或 std::wistream 定義 operator>>。
- 目標型別必須要有 default constructor 和 copy constructor。
所以基本上,只要有針對自己定義的型別,也定義出 IO Stream 的 operator<< 和 operator>>,應該就可以用了!
一些小細節
不過實際上 Heresy 在用的時候,還是有遇到一些問題。像以前面 CVector2 的例子來說,雖然已經有定義了 operator<< 和 operator>>,但是還是會有問題的!像下面的例子,想直接使用 lexical_cast 把字串 sTmp 轉換成為 CVector2 的話,語法雖然沒問題,但是在執行時是會出錯的!
string sTmp = "12 34";
CVector2 v = boost::lexical_cast< CVector2 >( sTmp );
主要的原因呢,應該是由於 Boost 的 lexical_cast 會去設定使用的 istream 為 noskipws、不忽略空白(參考);而也因此,才會造成 istream& operator>>( istream& iS, CVector2& v ) 這個函式在直接使用 stringstream 的時候因為預設會略調空白的資料、而可以正常運作,但是使用 lexical_cast 時卻因為特別把空白也抓出來做處理了,反而造成轉換的錯誤。
而解決方法呢?其實也滿簡單的,只要在進行處理前,先強制設定回 skipws 應該就可以了∼修正後的函式,也就變成了:
istream& operator>>( istream& iS, CVector2& v )
{
iS >> std::skipws >> v.x >> v.y;
return iS;
}
理論上,這樣應該就不會有問題、可以直接用 lexical_cast 來處理自訂的 CVector2 了∼
不過另外要注意的是,實際上 Boost 的 lexical_cast 在做處理的時候,做了很多例外狀況的判斷、處理,算是相當嚴謹的;而也因此,他的效能並不會特別好!有興趣的話,可以參考看看《Very poor boost::lexical_cast performance》這串討論。不過基本上,由於它的效率真的比較差一些,所以如果是轉換的效能很重要的話,建議還是換比較快的方法來寫會比較合適。