在幾年前,Heresy 曾經寫了一篇《在文字和數字間轉換:boost::lexical_cast》,來介紹 Boost C++ Libraries 裡的 lexical_cast 這個函式庫;他的基本功能,就是來將文字、和其他的資料型別做轉換。
在 Heresy 當時看來,這個函式庫算是相當方便的~就算是自定義的型別,也可以透過實作 operator << 和 >> 來支援 lexical_cast。整體來說,他應該是可以簡化一些型別轉換時的程式寫法的。
不過,他還是有一些限制與問題在。除了效能不算很好外,他的錯誤處理方法也只有 exception 一種,此外更不支援格式化的設定,這讓他在某些情況下,不是很好用。
所以,在 Boost 1.59.0 的時候,Boost 又加入了另一個名為「Convert」的函式庫(官網),希望可以解決 lexical_cast 的這些問題。
基本架構介紹
boost::convert 基本上分成兩個主要的部份,一個是他的函式介面、也就是 boost::convert<>() 這個函式;另一個則是一個可以擴展的「轉換器」(convertor)的架構。
透過這樣的架構,開發者可以使用(接近)同樣的寫法,透過切換不同的轉換器,來達到更換換機制、方法、效果的目的,使用上的彈性算是相當地大~
而 boost::convert 預設也提供了五種轉換器,可以直接使用:
- 基於 boost::lexical_cast 的轉換器
- 基於 printf / scanf 的轉換器
- 使用 strtol 系列函式的轉換器
- 基於 std::stringstream 的轉換器
- 基於 boost::spirit(官網)的轉換器
所以如果本來是使用 lexical_cast 來轉換的程式,其實也不用擔心改用 boost::convert 會出現轉換結果不一致的狀況~
主要介面
boost::convert 主要的函式介面是:
boost::optional<TypeOut> boost::convert(TypeIn const&, Converter const&);
其中,第一個引數是要轉換的變數,第二個引述則是要使用的轉換器。
而他的回傳值,則是使用 boost::optional 這個函式庫的型別(官網);他基本上是一個「可以沒有值」的容器,有點類似會幫你做好資源管理、單一功能的變數指標。
透過 optional 這個容器來回傳,可以確保在呼叫 convert() 進行轉換時,即使轉換失敗也不會丟出例外;所有的轉換結果判斷,都可以透過他來做檢查。
在某些狀況下,optional 算是一個很實用的容器,而實際上,在 C++17 也已經把他引入標準函式庫內了(參考)。
當然,convert 也還有提供一些其他的函式介面,基本上是針對不同的轉換錯誤處理方式做設計的,這邊就先跳過了。
基本範例
而下面,則是一個 boost::convert 最簡單的範例:
#include <iostream> #include <string> #include <boost/convert.hpp> #include <boost/convert/lexical_cast.hpp> void main() { boost::cnv::lexical_cast cnv; boost::optional<int> v = boost::convert<int>("1234", cnv); if (v) { int iRes = *v; //int iRes = v.value(); } }
在這個範例裡面,是透過 boost::convert,使用 lexical_cast 這個轉換器把「1234」這個字串,轉換成整數。
這邊為了使用 boost::convert<>() 這個函式,必須要先引入 boost/convert.hpp 這個 header 檔。
而根據要使用的 convertor 的不同,則也需要引入對應的 header 檔;像這邊是使用 boost::cnv::lexical_cast 這個轉換器,所以就需要再引入 boost/convert/lexical_cast.hpp 這個檔案。
在實際使用時,則需要先產生轉換器的實體,在上面的範例裡就是「cnv」這個物件;而之後就可以透過 boost::convert<>() 這個函式來做轉換了~
他回傳的結果是「v」,型別是 boost::optional<int>,代表是一個允許沒有值得整數。在使用前,可以先透過 if(v) 來檢查他是否有值,然後再取得他的值;這邊取得它實際的值的方法除了可以用類似指標的方法來做(*v),或是透過呼叫 value() 這個函式來做。
其他的轉換器
前面也有提過了,boost::convert 有提供五種不同方法的轉換器,如果要使用的話,都有對應的 header 檔,下面就是列表:
#include <boost/convert/lexical_cast.hpp> boost::cnv::lexical_cast cnv1; #include <boost/convert/strtol.hpp> boost::cnv::strtol cnv2; #include <boost/convert/spirit.hpp> boost::cnv::spirit cnv3; #include <boost/convert/stream.hpp> boost::cnv::cstream cnv4; boost::cnv::wstream wcnv4; #include <boost/convert/printf.hpp> boost::cnv::printf cnv5;
其中比較特別的,應該是基於 stream 的版本,因為只有他針對寬字元字串的的版本。
在使用時,只要把上面的 cnv1 等轉換器取代之前程式中的 cnv 就可以了。
而這些不同的轉換器,有的有不同的功能、轉換效能也不盡相同,基本上就是要看自己的需求,來決定要用哪個了~
下面就是由官方提供的測試數據(gcc 5.4.0 + Ubuntu 16.04 x64)繪製的效能比較表(原始數據):
spirit |
strtol |
lcast |
scanf |
stream |
|
str to int |
0.27 |
0.35 |
0.92 |
2.11 |
2.09 |
str to lng |
0.69 |
0.31 |
1.28 |
2.07 |
2.50 |
str to dbl |
0.73 |
1.06 |
7.95 |
2.87 |
5.10 |
int to str |
1.96 |
1.39 |
2.52 |
3.49 |
2.58 |
lng to str |
2.45 |
1.51 |
2.32 |
3.30 |
2.63 |
dbl to str |
6.62 |
4.46 |
28.69 |
20.60 |
14.16 |
str to user type |
0.07 |
0.36 |
0.18 |
||
user type to str |
0.06 |
0.58 |
0.09 |
這邊可以看到,其實 strtol 這種 c-style 的函式,效能還是最好的,而 boost::spirit 的效能也算是相對優異;相較於此 lexical_cast、scanf/printf、stream 等方法的效能,就相對差了不少了…
一般使用情況應該是影響不大,但是如果這些轉換是效能瓶頸的話,就得慎選轉換器了。
而如果想要自己撰寫一個轉換器的話,也可以自己撰寫一個可呼叫的物件(callable object),並實作各種轉換函式就可以了。
像下面的 MyConvertor,就是一個可以接受任何型別,但是實際上什麼事都沒做轉換器。
struct MyConvertor { template<typename TypeOut, typename TypeIn> void operator()(TypeIn const& value_in, boost::optional<TypeOut>& result_out) const {} };
使用預設轉換器
如果覺得每個地方都還得指定轉換器很麻煩的話,boost::convert 也有提供介面,可以直接使用預設的轉換器,藉此簡化更換轉換器的寫法。
下面就是一個簡單的範例:
#include <iostream> #include <string> #include <boost/convert.hpp> #include <boost/convert/lexical_cast.hpp> struct boost::cnv::by_default : boost::cnv::lexical_cast {}; void main() { boost::optional<int> v = boost::convert<int>("1234"); int iRes = v.value_or(-1); }
boost::convert 的設計,是在沒有指定轉換器的時候,會去使用一個名為「boost::cnv::by_default」的轉換器;而這個型別是沒有定義的,如果要使用,就要自己定義他。
所以這邊的做法,就是先定義一個 boost::cnv::by_default 的結構,讓他去繼承想要使用轉換器,這樣就可以了~
自訂型別的轉換
如果針對自己定義的型別要做轉換的話,boost::convert 也允許開發者可以透過幾種不同的方法,來讓自訂的型別也可以轉換。
首先,這邊先假設有一個自定義的型別如下:
struct Vector2 { float fX; float fY; };
如果想要透過 boost::convert 的介面,讓它可以和字串之間互相轉換的話,最一般的方法應該就是透過擴展既有的轉換器,附加額外的轉換函式來達成。
像下面的 MyConvertor 就是一個繼承 boost::cnv::lexical_cast、又可以轉換 Vector2 的轉換器了。
struct MyConvertor : boost::cnv::lexical_cast { void operator()(const std::string& sInput, boost::optional<Vector2>& vOutput) const
{ std::istringstream iss(sInput); float x, y; iss >> x >> y; vOutput = Vector2{ x, y };
} void operator()(const Vector2& vInput, boost::optional<std::string>& sOutput) const
{ std::ostringstream oss; oss << vInput.fX << " " << vInput.fY; sOutput = oss.str();
} };
可以看到,這邊基本上就是實作了兩個 operatior(),分別是把 std::string 轉換成 boost::optional<Vector2>,以及把 Vector2 轉換成 boost::optional<std::string>;而這邊的轉換核心,是透過 stringstream 來進行的。
當然,除了去擴展既有的轉換器外,其實也可以獨立建立一個轉換器,專門用來轉換 Vector2,這也算是一種玩法;要這樣做的話,只要修改 MyConvertor,不要讓他去繼承 boost::cnv::lexical_cast 就可以了。
不過這樣做的話,這個 MyConvertor 就不能轉換其他型別的資料了。
而除了去擴增既有的轉換器外,根據使用的轉換器的不同,也有其他方法,可以讓 boost::convert 支援自訂的型別。
以 boost::cnv::cstream 和 boost::cnv::lexical_cast 來說,其實也可以透過定義自訂型別的 stream 函式、讓他可以支援。
以 Vector2 來說,寫法就是去定義他的 output operator 和 input operator,下面就是一個簡單的範例:
std::istream& operator >> (std::istream& iss, Vector2& rVal) { iss >> std::skipws >> rVal.fX >> rVal.fY; return iss; } std::ostream& operator<<(std::ostream& oss, const Vector2& rVal) { oss << rVal.fX << " " << rVal.fY; return oss; }
透過定義對應 Vector2 的 operator<< 和 operator>>,boost::convert 的 boost::cnv::cstream 和 boost::cnv::lexical_cast 這兩個轉換器,就可以直接處理 Vector2 這個型別的資料轉換了。
不過,這個方法也僅對 boost::cnv::cstream 和 boost::cnv::lexical_cast 這兩個轉換器有用;對於剩下的轉換器,就算定義了這兩個函式,boost::convert 還是沒辦法處理 Vector2 的。
相對的,如果是要讓其他的轉換器也可以轉換 Vector2 的話,是要去定義下面兩個函式:
void operator>>(const std::string& sInput, boost::optional<Vector2>& vOutput) { std::istringstream iss(sInput); float x, y; iss >> x >> y; vOutput = Vector2{ x, y }; } void operator>>(const Vector2& vInput, boost::optional<std::string>& sOutput) { std::ostringstream oss; oss << vInput.fX << " " << vInput.fY; sOutput = oss.str(); }
如此一來,像是 boost::cnv::printf 等,本身不是依靠 iostream 來實作的轉換器,也就可以用來支援 Vector2 了。
不過比較討厭的是,如果只實作這兩個函式的話,是不能使用 boost::cnv::cstream 和 boost::cnv::lexical_cast 這兩個轉換器來轉換 Vector2 的。
這篇大概就先寫到這裡了。
而實際上,boost::convert 如果是使用 boost::cnv::cstream 和 boost::cnv::strtol 這兩個轉換器的話,是還有支援一些額外的格式設定的~這部分就請先自行參考官方文件的《boost::cnv::stream Converter》和《boost::cnv::strtol Converter》這兩份文件了。
不過以 Heresy 自己來說,如果要在轉換成文字時要做比較複雜的格式控制的話,應該會回過頭去用 boost::format 吧?