文章目錄
- 類型轉換運算符
- 概念
- 避免過度使用類型轉換函數
- 解決上述問題的方法
- 轉換為 bool
- 顯式的類型轉換運算符
- 類型轉換二義性
- 重載函數與類型轉換結合導致的二義性
- 重載運算符與類型轉換結合導致的二義性
類型轉換運算符
概念
類型轉換運算符(conversion operator)是類的一種特殊成員函數。負責將一個類類型的值轉換成其他類型。
operator type() const ;
其中 type
表示某種類型。類型轉換運算符可以面向任意類型(除了 void
之外)進行定義,只要該類型能作為函數的返回類型。因此,我們不允許轉換成數組或者函數類型,但允許轉換成指針(包括數組指針及函數指針)或者引用類型。
一個類型轉換函數必須是類的成員函數;它不能聲明返回類型,形參列表也必須為空。類型轉換函數通常不應該改變待轉換對象的內容,因此,應該是const。
運用實例,定義一個簡單的類,令其表示 0~255
之間的一個整數:
構造函數將算術類型的值轉換成 SmallInt
對象,而類型轉換運算符將 SmallInt
對象轉換成 int
:
SmallInt si;
si = 4; // 將 4 隱式轉換成 SmallInt,然后調用 SmallInt::operator=
si + 3; // 首先將 si 隱式地轉換成 int,然后執行整數的加法
盡管編譯器一次只能執行一個 我們定義的類型轉換(如上面的構造函數/類型轉換運算符),但可以將其搭配 內置類型轉換(如double可以轉換成int) 實現二次轉換。
// 內置類型轉換將 doulbe 實參轉換成 int
SmallInt si = 3.14; // 調用 SmallInt(int) 構造函數,然后調用拷貝構造函數
// SmallInt 的類型轉換運算符將 si 轉換成 int
si + 3.14; // 內置類型將所得的 int 繼續轉換成 double
盡管類型轉換函數不負責指定返回類型,但實際上每個類型轉換函數都會返回一個對應類型的值:
避免過度使用類型轉換函數
- 類型轉換可能具有誤導性
例如,假設某個類表示 Date
,我們也許會為它添加一個從 Date
到 int
的轉換。然而,類型轉換函數的返回值應該是什么?
- 一種可能的解釋是,函數返回一個十進制數,依次表示年、月、日,例如,
July 30,1989
可能轉換為int
值19890730
。 - 同時還存在另外一種合理的解釋,即類型轉換運算符返回的
int
表示的是從某個時間節點(比如January 1,1970
)開始經過的天數。
問題在于 Date
類型的對象和 int
類型的值之間不存在明確的一對一映射關系。因此在此例中,不定義該類型轉換運算符也許會更好。作為替代的手段,類可以定義一個或多個普通的成員函數以從各種不同形式中提取所需的信息。
- 類型轉換運算符可能產生意外結果
對于類來說,定義向 bool
的類型轉換還是比較普遍的現象。
int i = 42;
cin << i; // 如果向 bool 的類型轉換不是顯式的,則該代碼在編譯器看來是合法的
因為 istream
本身并沒有定義 <<
,所以本來代碼應該產生錯誤。然而,該代碼能使用 istream
的 bool類型轉換運算符
將 cin
轉換成 bool
,而這個 bool值
接著會被提升成 int
并用作內置的左移運算符的左側運算對象。這樣一來,提升后的 bool值(1或0)
最終會 被左移42個位置。 這一結果顯然與我們的預期大相徑庭。
解決上述問題的方法
轉換為 bool
- 標準庫的早期版本中,
IO
類型定義了向void*
的轉換規則,以求避免上述問題。 - 在
C++11
標準中,IO
標準庫通過定義一個向bool
的顯式類型轉換實現同樣的目的。
其實我們在編程中經常用到 IO
類型定義的 operator bool
:
while(std::cin >> value)
為了對條件求值,cin
被 istream operator bool
類型轉換函數隱式地執行了轉換。如果 cin
的條件狀態是 good
,則該函數返回為真;否則該函數返回為假。(這部分知識可以看我之前的博客)
向 bool
的類型轉換通常用在條件部分,因此 operator bool
一般定義成 explicit
的。
顯式的類型轉換運算符
為了防止上面第二點這樣的異常情況發生,我們可以使用 explicit
關鍵字。
SmallInt si = 3; // 正確:SmallInt 的構造函數不是顯式的
si + 3; // 錯誤:explicit阻止隱式類型轉換
static_cast<int>(si) + 3; // 正確:顯式地請求類型轉換
當類型轉換運算符是顯式的時,我們也能執行類型轉換,不過必須通過顯式的強制類型轉換才可以。
該規定存在一個例外,即,如果表達式被用作條件,則編譯器會將顯式的類型轉換自動應用于它。 換句話說,當表達式出現在下列位置時,顯式的類型轉換將被隱式地執行:
if
、while
及do
語句的條件部分for
語句頭的條件表達式- 邏輯非運算符(
!
)、邏輯或運算符(||
)、邏輯與運算符(&&
)的運算對象 - 條件運算符(
? :
)的條件表達式。
類型轉換二義性
如果類中包含一個或多個類型轉換,則必須確保在類類型和目標類型之間只存在唯一一種轉換方式。否則的話,我們編寫的代碼將很可能會具有二義性。
在兩種情況下可能產生多重轉換路徑:
- 第一種情況是 兩個類提供相同的類型轉換: 例如,當
A類
定義了一個接受B類
對象的轉換構造函數,同時B類
定義了一個轉換目標是A類
的類型轉換運算符。 - 第二種情況是 類定義了多個轉換規則,而某些轉換規則可以通過其他類型轉換實現。 這種情況多出現在算術運算符上。
通常情況下,不要為類定義相同的類型轉換,也不要在類中定義兩個及兩個以上轉換源或轉換目標是算術類型的轉換。
第一種情況舉例:
解決方法是顯式調用:
A a1 = f(b.operator A());
A a2 = f(A(b));
第二種情況舉例:
我們使用兩個用戶定義的類型轉換時,如果轉換函數之前或之后存在標準類型轉換,則標準類型轉換將決定最佳匹配到底是哪個:
short s = 42;
// 把 short 提升成 int 優于 提升成 double
// 上面的 long 則沒有int和double誰優于誰的規則,因此會有二義性
A a3(s); // A::A(int)
重載函數與類型轉換結合導致的二義性
有時會出現這種情況:
或這種情況:
雖然我們可以通過顯式地構造正確的類型而消除二義性:
manip(C(10)); // 調用 manip(const C&)
manip2(E(double(10))); // 調用 manip2(const E&)
但意味著程序的設計存在不足。
重載運算符與類型轉換結合導致的二義性
重載的運算符也是重載的函數。因此也遵從通用的函數匹配規則。例如,如果 a
是一種類類型,則表達式 a sym b
可能是:
a.operatorsym(b); // a 有一個 operatorsym 成員函數
operatorsym(a, b); // operatorsym 是一個普通函數
和普通函數不同,我們無法通過調用的形式區分當前調用的是成員函數還是非成員函數。
舉個例子:
- 第一條加法語句接受兩個
SmallInt
值并執行+
運算符的重載版本。 - 第二條加法語句具有二義性:因為我們可以把
0
轉換成SmallInt
,然后使用SmallInt
的+
;或者把s3
轉換成int
,然后對于兩個int
執行內置的加法運算。
如果我們對同一個類既提供了轉換目標是算術類型的類型轉換,也提供了重載的運算符,則將會遇到重載運算符與內置運算符的二義性問題。