Boost 的另一個型別轉換函式庫:Convert

| | 0 Comments| 09:10
Categories:

在幾年前,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::cstreamboost::cnv::strtol  這兩個轉換器的話,是還有支援一些額外的格式設定的~這部分就請先自行參考官方文件的《boost::cnv::stream Converter》和《boost::cnv::strtol Converter》這兩份文件了。

不過以 Heresy 自己來說,如果要在轉換成文字時要做比較複雜的格式控制的話,應該會回過頭去用 boost::format 吧?

Leave a Reply

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