以下是 John Maddock 和 Steve Cleary 於 2000 年 10 月號的 Dr Dobb's Journal 中發表的文章「C++ Type traits」的更新版本。
泛型程式設計(撰寫可處理符合一組需求的任何資料類型的程式碼)已成為提供可重複使用程式碼的首選方法。然而,在泛型程式設計中,有時「泛型」並不足夠 - 有時類型之間的差異對於有效率的泛型實作來說太大了。這時 traits 技術就變得重要了 - 透過將需要在類型基礎上考慮的那些屬性封裝到 traits 類別中,我們可以盡量減少必須因類型而異的程式碼量,並最大化泛型程式碼的數量。
考慮一個範例:在處理字串時,一個常見的操作是確定以 null 結尾的字串的長度。顯然可以編寫可執行此操作的泛型程式碼,但事實證明有更有效的方法可用:例如,C 函式庫函式 strlen
和 wcslen
通常是以組語撰寫的,並且在適當的硬體支援下,可以比用 C++ 撰寫的泛型版本快得多。C++ 標準函式庫的作者意識到了這一點,並將 char
和 wchar_t
的屬性抽象到類別 char_traits
中。使用字串的泛型程式碼可以簡單地使用 char_traits<>::length
來確定以 null 結尾的字串的長度,並確信 char_traits
的特化將使用它們可用的最適合的方法。
類別 char_traits
是一個經典範例,其中將類型特定的屬性集合封裝在單一類別中 - Nathan Myers 稱之為 包裹類別[1]。在 Boost 類型特性函式庫中,我們[2]編寫了一組非常特定的 traits 類別,每個類別都封裝了 C++ 類型系統中的單一特性;例如,類型是指標或參考類型嗎?或者類型是否具有簡單的建構函式或 const 限定符?類型特性類別共用一個統一的設計:如果類型具有指定的屬性,則每個類別都繼承自類型 true_type,否則繼承自 false_type。正如我們將展示的那樣,這些類別可以用在泛型程式設計中,以確定給定類型的屬性並引入適合該情況的優化。
類型特性函式庫還包含一組對類型執行特定轉換的類別;例如,它們可以從類型中移除頂層的 const 或 volatile 限定符。執行轉換的每個類別都會定義單一的 typedef 成員 type
,該成員是轉換的結果。所有類型特性類別都在命名空間 boost
內定義;為了簡潔起見,大多數程式碼範例中省略了命名空間限定。
類型特性函式庫中包含的類別太多了,無法在此處給出完整的實作 - 有關完整詳細資訊,請參閱 Boost 函式庫中的原始碼 - 但是,大多數實作都相當重複,因此在這裡我們只會讓您了解某些類別的實作方式。從函式庫中最簡單的類別 is_void<T>
開始,只有當 T
是 void
時,它才會繼承自 true_type
。
template <typename T> struct is_void : public false_type{}; template <> struct is_void<void> : public true_type{};
在這裡,我們定義了樣板類別 is_void
的主要版本,並在 T
是 void
時提供完整特化。雖然樣板類別的完整特化是一項重要的技術,但有時我們需要介於完全泛型解決方案和完全特化之間的解決方案。這正是標準委員會定義部分樣板類別特化的情況。例如,考慮類別 boost::is_pointer<T>
:在這裡,我們需要一個主要版本來處理 T 不是指標的所有情況,以及一個部分特化來處理 T 是指標的所有情況。
template <typename T> struct is_pointer : public false_type{}; template <typename T> struct is_pointer<T*> : public true_type{};
部分特化的語法有些神秘,並且很容易就佔用一篇文章的篇幅;像完整特化一樣,為了寫出類別的部分特化,您必須先宣告主要樣板。部分特化在類別名稱後包含一個額外的 <...>,其中包含部分特化參數;這些參數定義了將繫結到該部分特化的類型,而不是預設樣板。部分特化中可以出現的規則有些複雜,但根據經驗,如果您可以合法地編寫以下形式的兩個函式多載
void foo(T); void foo(U);
那麼您也可以編寫以下形式的部分特化
template <typename T> class c{ /*details*/ }; template <typename T> class c<U>{ /*details*/ };
此規則絕非萬無一失,但它相當容易記住,並且足夠接近實際規則,可以用於日常使用。
作為部分特化的更複雜範例,請考慮類別 remove_extent<T>
。此類別定義一個單一的 typedef 成員 type
,其類型與 T 相同,但會移除任何頂層陣列界限;這是對類型執行轉換的 traits 類別的範例
template <typename T> struct remove_extent { typedef T type; }; template <typename T, std::size_t N> struct remove_extent<T[N]> { typedef T type; };
remove_extent
的目的是:想像一個將陣列類型作為樣板參數傳遞的泛型演算法,remove_extent
提供了一種確定陣列基礎類型的方法。例如,remove_extent<int[4][5]>::type
將評估為類型 int[5]
。此範例也顯示,部分特化中的樣板參數數量不必與預設樣板中的數量相符。但是,類別名稱後出現的參數數量必須與預設樣板中參數的數量和類型相符。
作為如何使用類型特性類別的範例,請考慮標準函式庫演算法 copy
template<typename Iter1, typename Iter2> Iter2 copy(Iter1 first, Iter1 last, Iter2 out);
顯然,編寫適用於所有迭代器類型 Iter1
和 Iter2
的泛型 copy 版本沒有問題;但是,在某些情況下,最好透過呼叫 memcpy
來執行複製操作。為了以 memcpy
實作 copy,需要滿足以下所有條件
Iter1
和 Iter2
都必須是指標。Iter1
和 Iter2
都必須指向相同的類型 - 不包含 const 和 volatile 限定符。Iter1
指向的類型必須具有簡單的指派運算子。簡單的指派運算子是指該類型是純量類型[3],或者
如果滿足所有這些條件,則可以使用 memcpy
而不是使用編譯器產生的指派運算子來複製類型。類型特性函式庫提供了一個類別 has_trivial_assign
,使得只有當 T 具有簡單的指派運算子時,has_trivial_assign<T>::value
才為 true。此類別對於純量類型「正常運作」,但對於也恰好具有簡單指派運算子的 class/struct 類型必須明確特化。換句話說,如果 has_trivial_assign 給出錯誤答案,它將給出「安全的」錯誤答案 - 不允許簡單的指派。
在範例中提供了一個最佳化版本的 `copy` 程式碼,它會在適當的時候使用 memcpy
。程式碼首先定義了一個樣板函式 do_copy
,它執行一個「慢但安全」的複製。傳遞給這個函式的最後一個參數可能是 true_type
或 false_type
。接下來是一個 `do_copy` 的重載版本,它使用 memcpy
:這次迭代器必須實際上是指向相同類型的指標,並且最後一個參數必須是 true_type
。最後,copy
的版本會呼叫 do_copy
,並將 has_trivial_assign<value_type>()
作為最後一個參數傳遞:這將在適當的時候分派到最佳化版本,否則它將呼叫「慢但安全」的版本。
這些專欄中經常重複一句話:「過早最佳化是萬惡之源」 [4]。所以必須問這個問題:我們的最佳化是否過早?為了說明這一點,表 1 中顯示了我們版本的 `copy` 與傳統通用複製[5]的計時比較。
顯然,在這種情況下,最佳化確實產生了差異;但為了公平起見,計時是經過調整的,以排除快取未命中效應 - 如果沒有這樣做,準確比較演算法將變得困難。然而,或許我們可以對過早最佳化的規則增加一些警告
表 1.1. 使用 `copy<const T*, T*>` 複製 1000 個元素所花費的時間(時間單位為微秒)
版本 |
T |
時間 |
---|---|---|
「最佳化」的複製 |
char |
0.99 |
傳統的複製 |
char |
8.07 |
「最佳化」的複製 |
int |
2.52 |
傳統的複製 |
int |
8.02 |
最佳化的複製範例展示了如何使用類型特性在編譯時執行最佳化決策。類型特性的另一個重要用途是允許程式碼在沒有使用過度偏特化 (partial specialization) 的情況下進行編譯。這是透過將偏特化委託給類型特性類別來實現的。我們使用這種形式的範例是一個可以保存參考的 `pair` [6]。
首先,讓我們檢視 std::pair
的定義,為了簡單起見,省略了比較運算子、預設建構子和樣板複製建構子
template <typename T1, typename T2> struct pair { typedef T1 first_type; typedef T2 second_type; T1 first; T2 second; pair(const T1 & nfirst, const T2 & nsecond) :first(nfirst), second(nsecond) { } };
現在,這個 "pair" 目前無法保存參考,因為建構子會要求取得對參考的參考,這是目前不合法的 [7]。讓我們考慮一下建構子的參數必須是什麼,才能允許 "pair" 保存非參考類型、參考和常數參考
稍微熟悉一下類型特性類別,我們就可以建構一個單一的映射,允許我們從包含的類別的類型中判斷參數的類型。類型特性類別提供了一個轉換 add_reference,它會在其類型上加入參考,除非它已經是一個參考。
表 1.3. 使用 add_reference 合成正確的建構子類型
|
|
|
---|---|---|
T |
const T |
const T & |
T & |
T & [8] |
T & |
const T & |
const T & |
const T & |
這允許我們為可以包含非參考類型、參考類型和常數參考類型的 `pair` 建構一個主要的樣板定義
template <typename T1, typename T2> struct pair { typedef T1 first_type; typedef T2 second_type; T1 first; T2 second; pair(boost::add_reference<const T1>::type nfirst, boost::add_reference<const T2>::type nsecond) :first(nfirst), second(nsecond) { } };
加入標準比較運算子、預設建構子和樣板複製建構子(它們都相同),你就擁有一個可以保存參考類型的 std::pair
!
相同的擴充功能可以使用 `pair` 的偏樣板特化來完成,但是要以這種方式特化 `pair`,需要三個偏特化加上主要樣板。類型特性允許我們定義一個單一的主要樣板,它可以自動調整為任何這些偏特化,而不是使用強硬的偏特化方法。以這種方式使用類型特性允許程式設計師將偏特化委託給類型特性類別,從而產生更易於維護和理解的程式碼。
我們希望在這篇文章中,我們能夠讓您了解類型特性的全部內容。boost 文件中列出了更完整的可用類別清單,以及更多使用類型特性的範例。樣板使 C++ 使用者能夠利用泛型程式設計帶來的程式碼重用優勢;希望本文已經表明,泛型程式設計不必屈就於最低的共同分母,而且樣板可以既是最佳化的,又是通用的。
作者要感謝 Beman Dawes 和 Howard Hinnant 在撰寫本文時提供的寶貴意見。