本節書摘來自異步社群出版社《imperfect c++中文版》一書中的第2章,第2.7節,作者: 【美】martin d.carroll , margaret a.ellis,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視。
c++代碼設計與重用
2.7 轉型
程式庫設計者必須充分重視隐式轉型(implicit conversion)。在c++中,有兩種方法可以用來定義從類型from到類型to的隐式轉型。第一種,我們可以在類to中定義一個隻含一個參數的構造函數(并且沒有其他的預設參數):
或者,我們可以在類from中定義一個轉型操作:
假如上面這兩個函數中的一個(也隻能是一個)存在,那麼當一個類型為from的參數傳遞給需要類型為to(或者const to&)的參數時,就會發生隐式轉型:
2.7.1 多重所有權(multiple ownership)
如果from::operator to和to(const from&)兩個操作都已經聲明了,那麼對f的調用将會是二義性的(ambiguous):
增加一個強制轉型(指(to)from)并不能消除二義性;但多重所有權問題(如此命名是由于類from和類to都擁有轉型操作)是可以很容易避免的,隻要from和to的設計者更加小心謹慎,不要提供兩個轉型操作就可以了。
對稱轉型的存在—就是說,一個從from到to的隐式轉型和一個從to到from的隐式轉型都存在—并不會導緻相同的二義性問題。
實際上,現實的類一般都提供對稱轉型。考慮一個用于表述正規表達式[sm77]的類regex,因為有必要用一個字元串來構造一個regex對象,并且有必要把一個正規表達式解釋成一個字元串,是以一個真實的regex類提供了和string類的對稱轉型:
如果不能改變類string的定義,那麼我們就隻能都在類regex内定義這兩個轉型操作了。
2.7.2 敏感轉型
一個從from到to的隐式轉型,如果它表述的是一個從from到to的自然映射,或者是使用者想要默許發生的轉型,我們就說這個轉型是敏感的(sensible)。實際上,大多數轉型都應該是敏感的(我們會在下一節讨論特殊情況)。
下面讓我們來考慮幾個有關敏感性(sensibility)的例子。假設我們是一個數學程式庫的設計者,在我們的程式庫裡,類rational和類complex分别描述有理數和複數。假如我們隻考慮int、double、rational和complex 4種類型,就會有12種可能的隐式轉型;那麼,這些轉型中哪些是敏感的呢?
complex到int的轉型
很顯然,這個轉型不是敏感的(sensible)。盡管我們有可能定義任何從複數到整數的映射,但這種定義肯定不是自然的。
int到complex的轉型
這是一個很明顯的映射:從整數x映射到複數x+(0)(),并且,使用者大多數都希望整數可以預設地轉化為複數。是以,這個轉型是敏感的。
rational到double的轉型
将一個rational對象映射到一個double對象,這個double對象儲存的值是rational對象的分子除以分母得到的近似值,整個過程将可能損失一定的精度。盡管這個轉型有時可以被認為是自然的,但是使用者往往不想讓這種轉型預設地進行(因為精度可能損失)。是以,這種轉型不是敏感的。
double到rational的轉型
如果我們認為double描述的是實數集合,那麼從double到rational将沒有自然的映射;但實際上,每個double對象表述的是一個有限小數,并且有限小數和有理數有着很好的自然映射,是以,這個轉型有可能是敏感的。
complex到rational的轉型
由于不存在使用者希望發生的、從complex到rational的自然映射,是以這個轉型不是敏感的。
rational到complex的轉型
這個轉型是比較複雜的。每一個有理數同時也可以是一個複數,是以我們可以認為存在一個從rational到complex的映射。然而,如果complex隻能以x+(y)()的形式來表示複數,其中x和y都是double類型,那麼這個轉型就不是敏感的了,原因和rational到double的轉型不是敏感的原因相同(即精度損失)。
從上面這些例子可以看出,經過了詳細的分析,很多隐式轉型都不是敏感的,是以,我們就不應該提供這類轉型操作。練習2.6要求讀者判斷剩餘轉型的敏感性。
注意,對于某些非敏感的隐式轉型,如果我們的程式确實需要它所實作的功能,那麼我們可以把這種轉型實作為顯式轉型。例如,盡管從rational到double的隐式轉型不是敏感的,并且會有精度的損失,但使用者卻可以顯式地(explicitly)将rational對象轉型為double對象;我們可以在數學程式庫裡提供這個函數:
2.7.3 不敏感轉型
回想一下2.4.1節的pool類,如2.4.1節所述,pool的構造函數需要包含一個大小參數:
此外,我們也沒有給這個函數傳遞其他的參數。是以,我們會得到一個單參數的構造函數,并由它來建立一個轉型函數。遺憾的是,從size_t到pool的轉型并不是敏感的—幾乎沒有使用者會把一個size_t對象預設地轉化為一個pool對象:
我們可以用兩種方法來解決上面這個問題。第一種,我們可以隻提供構造函數,把避免隐式轉型的工作留給使用者完成;第二種,我們可以定義一個中間類:
因為對函數的實參,如果它的轉型是使用者定義的,那麼c++是不能(隐式地)執行多于一次的轉型的;這個設計将會導緻編譯器拒絕編譯下面的錯誤代碼:
然而,定義一個中間類有很大的缺點:它使pool類的了解和使用更加困難。現在如果想要構造一個pool對象,使用者應該這樣編碼:
大多數使用者都會傾向于使用雖容易産生錯誤,但更加簡單的接口;是以,程式庫有時也提供不敏感的(nonsensible)轉型。
我們可以這樣來定義一個類型的轉型數目(fanout):它就是這個類型可以隐式轉換為其他類型的數目。很大的轉型數目往往不是我們所期望的,因為它很容易導緻二義性的發生。例如,假設類from可以轉型為兩種類型:to和another_to,那麼下面的函數調用就存在二義性:
增加一個強制轉型(cast)就可以解決這個二義性問題:
然而,如果可能的話,c++程式庫不應該強迫它的使用者在他們的代碼中使用強制轉型(cast)。是以,c++程式庫應該避免大的轉型數目。幸運的是,隻提供敏感轉型的程式庫一般隻具有小的轉型數目,特殊情況就是一些諸如int和char 的内建類型;因為很多類定義了具有這種類型單參數的構造函數,于是int和char 就具有很大的轉型數目。為了避免二義性問題,程式庫使用者無論在什麼時候,都應該避免依賴從内建類型到程式庫定義類型之間的隐式轉型。
本文僅用于學習和交流目的,不代表異步社群觀點。非商業轉載請注明作譯者、出處,并保留本文的原始連結。