模板(template)與泛型

  • 模板與泛型是現代 C++ 程式中重要的內容,很多大型專案(如 QuantLib 或是 C++ 的標準程式庫)大量使用模板與泛型相關技術。

  • 即使開發時不常使用,但在閱讀一些重要程式庫時,仍舊無法避免必須理解模板與泛型的重要觀念。

  • OOP 並不直接處理『型態在程序執行之前都未知』的情况。而是『多態』允許在執行時根據物件的實際型態來決定使用哪個方法。實際上,所有類別的定義都必须在編譯時已知。

  • 『泛型(Generics)』在編譯時就能知道型態。

  • C++ 中的模板(Templates)與泛型(Generics)是一種機制,允許開發人員編寫不依賴於具體型態的程式碼。

  • 容器(如 vector)、迭代器等都是『泛型』(類別模板)概念的實現。

  • 泛型為獨立於任何特定型態的編程技術。

  • 『模板(Templates)』為 C++ 泛型的基礎。

  • 『模板』就是創建一個『類別』或『函數』的藍圖或是公式。而這些藍圖或公式在未來將轉變為『具體』的類別或是函數。

    • 模板支援將型態作為參數的程式設計方式,從而實現對泛型程式設計實作。

    • 模板(Templates):C++ 模板是一種在『編譯期』進行『型態參數化』的方法,允許編寫通用的程式碼框架,可與不同的資料型態一起工作。而 C++ 中主要有兩種模板:

      • 函數模板:定義通用的函數,該函數可使用不同的資料型態進行操作。

      • 類別模板:定義通用的類別,可使用不同的資料型態來儲存和處理資料。

    • 在 C++ 中,『泛型(Generics)』一般與『模板(Templates)』被視為同義詞。

    • 泛型編程是一種編程風格,強調編寫出獨立於任何特定資料型態的程式碼。

  • 例如,可使用函數模板來創建一個通用的排序函數,該函數可以用於整數、浮點數甚至自定義對象,只要這些物件支援比較操作即可:

    template <typename T>
    void sort(T arr[], int size) {
        // 排序演算法實現
    }
    • T 是一個『型態』佔位符,可在呼叫函數時適用任何有效的 C++ 型態來做替換。該方法提高了程式碼的靈活性和可重用性。

模板概念

  • 當編譯器處理『模板定義』時,並不產生程式碼。只有當實例化出模板的一個特定版本時,編譯器才會產生程式碼。

  • 故當『使用』模板時(非定義時)模板時,編譯器才會產生程式碼。

  • 一般情況,當呼叫函數時,編譯器只需知道函數的宣告。同理,當使用類別型態的物件時,類別定義必須可知,則成員函數『定義』則不需已知,故可將『類別定義』與『函數宣告』放至『標頭檔 .h』中,而普通函數與類別的成員函數定義則放在『原始檔 .cpp』內。

  • 模板則不同:為了實例化一個模板版本,編譯器需要掌握『函數模板』或『類別模板』成員函數的定義。故模板的『標頭檔 .h』一般會包含『宣告』與『定義』

  • 模板編譯錯誤:

    • 第一階段:編譯模板。在此階段,編譯器通常不會發現很多錯誤。編譯器可以檢查語法錯誤,例如忘記分號或是變數名稱拼錯等。

    • 第二階段:編譯器遇到模板使用時。在此階段,編譯器仍然没有很多可檢查的。對於函数模板呼叫,編譯器一般會檢查時參數個數是否正確。並還能檢查參數型態是否匹配。而對於類別模板,編譯器則可檢查使用者是否提供了正確數量的模板實參。

    • 第三階段:模板實例化時。只有這個階段才能發現型態相關的錯誤。依赖於編譯器如何管理實例化,此類錯誤可能在連結期時才會報出錯誤。

函數模板

  • 函數模板可視為一個公式,可用來產生針對特定型態的函數版本(實例化)

    template <typename T>
    int compare(const T &v1, const T &v2)
    {
      if (v1 < v2) return -1;
      if (v2 < v1) return 1;
      return 0;
    }
  • 概念

    • 函數模板是一種定義通用函數的方式,使得函數可以操作多種型態的資料,而不必為每種資料型態重寫程式碼。在定義函數模板時,不是使用具體的資料型態(如 int, float 等),而是使用『型態參數(Type Parameters)』。

    • 函數模板的定義以 template 關鍵字開始,緊接著 <>,而 <> 後面跟著『模板參數列表(template parameter list)』。

    • 模板定義中,模板參數列表不能為空。

    • 『模板參數列表』內表示在函數定義中使用到的『型態』或『值』,與函數參數列表類似。

    • 使用模板時有時需指定『模板實參』,指定時也必須使用 <> 將模板實參包含進去。

    • 但有時又不需要指定模板實參,系統可根據一些線索推論出型態。

    • 當系統推論不出型態時,則會出現編譯錯誤。

    • 型態參數前必須使用關鍵字 classtypename(含義相同,可以互換使用):

    template <typename T>
    void swap(T& a, T& b) {
        T temp = a;
        a = b;
        b = temp;
    }
    • T 是一個模板參數,代表一種未指定的資料型態。

    • 當這個模板函數被呼叫時,編譯器會根據傳入參數的『實際型態』自動『實例化』一個具體版本的函數。

  • 使用函數模板(實例化(instantiate)函數模板)

    • 模板函數與一般函數一樣。當呼叫模板函數時,編譯器會根據呼叫該模板函數時提供的實參來推論模板參數列表內的型參型態,故通常不需要明確指定型態參數,編譯器會自動推斷出來。

    • 有時候,系統無法根據所提供的實參推論出模板參數,此時就需要使用 <> 主動提供模板參數。

    • 因此,在呼叫模板函數時,可先不看函數模板定義中 template<> 內有多少模板參數;主要是看函數模板內的函數名稱之後的參數數量。

    • 編譯器實例化一個模板時,其使用實際的模板實參代替對應的模板參數來建立一個新的『實例』。

    int a = 5, b = 10;
    swap(a, b); // 編譯器推斷出 T 為 int
    
    double x = 5.5, y = 10.5;
    swap(x, y); // 編譯器推斷出 T 為 double
  • 『非型態模板參數』

    • 除了定義『型態參數』,可以在模板中定義『非型態參數(nontype parameter)』

    • 『非型態參數(nontype parameter)』表示為一個『值』,而非型態。

    • 透過特定的型態名稱,而『非』關鍵字 classtypename 來指定『非型態參數(nontype parameter)』。

    • 當模板被實例化時,『非型態參數(nontype parameter)』可被『使用者提供』或由『編譯器推論』出的值所取代。

    • 『非型態參數(nontype parameter)』必須為『常數表達式』,使得編譯器可在編譯期實例化模板。

      template<int a, int b>
      int funcaddv2()
      {
        int addhe = a + b;
        return addhe;
      }
      
      int result = funcaddv2<12, 13>();  // 透過 <> 來傳遞參數。『顯式』指定模板參數。
      
      int a = 12;
      int result2 = funcaddv2<a, 13>();  // 錯誤:『非型態模板參數』必須是『常數表達式』。
      • 此例無『型態模板參數』,只有『非型態模板參數』。

      • 透過 <> 來傳遞參數。此為『顯式』指定模板參數。

      • 當系統推論不出型態時,則需『顯式』指定模板參數。

      template<typename T, int a, int b>
      int funcaddv3(T c)
      {
        int addhe = (int)c + a + b;
        return addhe;
      }
      
      int result = funcaddv3<int, 11, 12>(13);    
      std::cout << result << std::endl;           // 36
      
      int result2 = funcaddv3<double, 11, 12>(13);    
      std::cout << result << std::endl;           // 36,此時,系統會以用 <> 傳遞進去的型態為準,而不是以13推論出來的型態為準。
      template <unsigned N, unsigned M>
      int compare(const char (&p1)[N], const char (&p2)[M])
      {
        return strcmp(p1, p2);
      }
      compare("hi", "mom");
      • 編譯器會使用常數字面值的大小來替代 NM,從而實例化函數模板:

        int compare(const char (&p1)[3], const char (&p2)[4]);
    • 『非型態參數(nontype parameter)』可以是一個整數型態,或是一個指向物件或函數型態的『指標』或『左值參考』。

    • 綁定到指標或參考非型態參數的實參必須具有『靜態』的生存期。

    • 無法使用一個普通(非 static)局部變數,或動態物件作為指標或參考『非型態參數(nontype parameter)』的實參。

  • inlineconstexpr 的函數模板

    • 函數模板可以宣告為 inlineconstexpr

      // 正確
      template <typename T> inline T min(const T&, const T&);
      // 錯誤
      // inline template <typename T> T min(const T&, const T&);
  • 函數模板的『特化』

    • 有時候,可能需要對『特定型態』提供一個特殊的實現,這就是所謂的『模板特化』。

    • 『模板特化』就是告訴編譯器,當特定的型態被用作模板參數時,使用一個特定的函數實現。

    template <>
    void swap<double>(double& a, double& b) {
        // 特殊處理 double 型態的 swap
    }
    • 注意事項:

      • 型態推斷:在大多數情況下,編譯器可以自動推斷模板參數的型態。但有時你可能需要手動指定型態,尤其是當函數無法從上下文中推斷出正確的型態時。

      • 過度泛化:雖然函數模板可以提高程式碼的重用性,但過度泛化可能會導致程式碼難以理解和維護。因此,合理地使用模板對於寫出高質量的程式碼非常重要。

      • 編譯錯誤:由於模板函數是在編譯時實例化的,所以任何與型態相關的錯誤都會在編譯時出現。這種錯誤有時可能難以理解,特別是在複雜的模板實例化中。

      • 程式碼膨脹:每當使用新的型態實例化模板時,編譯器都會生成一份新的函數實現。如果濫用模板,可能導致可執行文件的大小顯著增加。

類別模板

  • 類別模板的主要目的是為了程式碼重用和型態安全。

    • 透過類別模板,可定義一套操作和屬性,而這些操作與屬性對於不同的資料型態來說是通用的。這意味著可使用相同的類別模板來創建操作不同資料型態的類別實例。
  • 編譯器無法為類別模板推論模板參數,故使用類別模板時,必須在模板名稱後面使用 <> 提供額外資訊(模板參數列表內的參數)。

  • 定義類別模板:

    • 類別模板的定義以 template 關鍵字開頭,後跟模板參數列表。例如:

      template <typename T>
      class Box {
      public:
          T contents;
          void set(T value) {
              contents = value;
          }
          T get() {
              return contents;
          }
      };
      • T 是一個模板參數,代表了一種未指定的資料型態。當你實例化這個類別模板時,可用實際的資料型態(如 int, double, string 等)來替換 T
  • 使用類別模板:

    • 實例化類別模板時,編譯器就會產生一個具體的類別。

    • 每種型態都會產生一個不同的類別。

      Box<int> intBox;
      intBox.set(123);
      
      Box<string> stringBox;
      stringBox.set("Hello World");
  • 類別模板的成員函數

    • 類別模板的成員函數定義可以寫在類別模板定義的標頭檔(.h)中。這種寫在類別模板定義中的成員函數會被隱式宣告為 inline 函數。

  • 類別模板的特化:

    • 有時候,對於某些特定的資料型態,你可能希望提供一個特殊的類別實現,這就是所謂的類別模板特化。在特化中,你可以為特定的模板參數提供一個不同的實現。例如:

      template <>
      class Box<char> {
      public:
          char contents;
          // 特殊的實現
      };
  • 注意事項:

    • 型別安全:類別模板增加了代碼的型別安全性,因為它們允許你在編譯時檢查型別錯誤。

    • 代碼複雜性:使用類別模板可能會使代碼變得更加複雜,尤其是在涉及多個模板參數和模板特化時。

    • 編譯器依賴:類別模板的行為可能會依賴於特定的編譯器和其模板實現,這可能會導致在不同編譯器間的兼容性問題。

  • 模板用於『標準模板程式庫(STL)』和其他許多程式庫內,提供了強大的資料結構和演算法。

  • 妥善使用類別模板可以大大提高程式碼的重用性和可維護性,但也需要開發者對模板有深入的理解,以避免引入不必要的複雜性。

  • 透過精心設計與使用模板,可建立强大且靈活的 C++ 程式。

typename

  • 做為模板參數的型態指示符

    • 在定義模板(無論是類別模板或是函數模板)時,typename 用來指定一個『泛型』型態参数。

    • 用於告訴編譯器,隨後跟隨的標識符應被視爲一個『型態』。

    • typename 可與 class 關键字互换使用。

    template <typename T>
    class Box {
        T value;
        // ...
    };
    
    template <typename T>
    void print(const T& value) {
        std::cout << value << std::endl;
    }
  • 用於指定依賴型態名稱

    • 在模板中,當定義一個依赖於模板参数的型態时,需要使用 typename 来告訴編譯器,特定的標識符表示一個型態。這種情況通常發生在『模板內部』,尤其是在處理嵌套依赖行態时。

      template <typename T>
      class MyClass {
          typename T::SubType value;  // 使用 typename 指定 T::SubType 是一个类型
          // ...
      };
    • T::SubType 可能是一個依赖於模板参数 T 的型態。由於編譯器在處理模板时無法確定 T::SubType 是否為一个型態,故 typename 關鍵字在此處是必须的,以避免歧義。

成員函數模板

  • 成員函数模板(Member Function Templates)允許在類別内部定義模板函數。

  • 其意指這些成員函数可針對多種数据型態操作,而無需為每種型態編寫特定單獨的函数。

  • 成员函数模板提高程式碼的靈活性與重用性,同時保持型態安全。

  • 定義成員函數模板

    • 成員函數模板的定義類似於普通模板函数的定義,但它們是在類別定義的内部。

    • 成員函數模板可使用類別模板參數,也可以有自己的模板參數。例如:

      class MyClass {
      public:
          // 成員函数模板
          template <typename T>
          void print(const T& value) {
              std::cout << value << std::endl;
          }
      };
      • print 是一个成員函数模板,其可接受任何型態的參數並將其輸出。
  • 使用成員函數模板

    • 使用成員函數模板與使用普通成員函数類似,但有時可能需要明確指定模板參數。

      MyClass obj;
      obj.print(123);           // 打印整数
      obj.print("Hello World"); // 打印字串
  • 成員函數模板與類別模板

    • 成員函数模板也可以在類別模板内部定義。在這種情况下,成員函數模板可使用類別模板的參數,也可以有自己的獨立模板參數。
    template <typename U>
    class MyClass {
    public:
        // 使用類別模板參數
        void doSomething(const U& value) {
            // ...
        }
    
        // 成員函数模板,有自己的模板參數
        template <typename T>
        void print(const T& value) {
            std::cout << value << std::endl;
        }
    };
  • 注意事項

    • 成員函數模板不能為虚函数(virtual)。

    • 若成員函数模板和一般成員函数重載,則需注意函數的選擇和調用規則,可能需要明確指定模板參數以幣免歧義。

    • 成員函數模板的使用提高程式碼的通用性,但也可能增加了複雜性,特别是在涉及模板嵌套與特化时。

模板顯式實例化與宣告

  • 模板顯式實例化(Explicit Template Instantiation)和模板宣告(Template Declaration)用於控制模板的編譯和連結方式。

  • 模板顯式實例化

    • 在 C++ 中,當使用模板定義一個函数或類別时,編譯器並不立即產生該模板的實際程式碼。相反,它會等到模板被實際用到(例如,創建模板類別的物件或呼叫模板函數)時才會進行實例化。然而,有時能想要『顯式』告诉編譯器在某個特定點產生模板的實例化程式码,此即為所谓的『模板顯式實例化』。

    • 顯式實例化的語法是在模板定義前加上 template 關键字與模板實例的具體型態。例如:

    template class MyClass<int>;    // 顯式實例化 MyClass 的 int 版本
    template void myFunction<double>(); // 顯式實例化 myFunction 的 double 版本
  • 模板宣告

    • 模板宣告(也被稱為顯示實例化宣告(Explicit Instantiation Declaration))是一種只宣告模板實例而不定意它的方式。

    • 模板宣告』在分離編譯時非常有用。當在一個檔案中宣告模板的顯式實例化時,編譯器不會在該檔案中產生模板的實例化程式碼,但編譯器知道該模板的實例在程式的其他部分已經或將會被實例化。

    • 模板宣告的語法是在模板定義前加上 extern template 關鍵字和模板實例的具體型態。例如:

      extern template class MyClass<int>;    // 顯式宣告 MyClass 的 int 版本
      extern template void myFunction<double>(); // 顯式宣告 myFunction 的 double 版本
    • 使用場景

      • 編譯時間:顯式實例化可减少編譯時間,因其避免在每個使用模板的原始檔中都進行模板的實例化。

      • 控制實例化:透過顯式實例化與宣告,可更好地控制模板實例化的位置與方式,這在處理大型專案與程式庫時非常有用,尤其是在希望減少編譯相依與編譯時間的情况下。

      • 避免連結錯誤:顯式實例化有助於防止連結錯誤,特别是在模板程式碼出現在多個檔案中時。

    • 模板顯式實例化與宣告是 C++ 模板中用於管理和優化編譯過程的重要技巧。正確使用這些技術可提高編譯效率,減少編譯時間,並有助更好地組織與控制模板的結構。

using定義模板別名與顯式指定模板參數

  • 使用 using 定義模板别名

    • 模板别名允許為複雜的模板型態提供更簡短、更易讀的名稱。

    • 在處理『嵌套模板』或模板參數較多的情况下特别有用。

    • 使用 using 關鍵字可創建這樣的别名。

    template <typename T>
    using Vec = std::vector<T>;
    
    Vec<int> intVector; // 等同於 std::vector<int>
  • 使用 using 顯式指定模板參數

    • using 也可用於在類別模板內部顯式指定某個已有模板的參數,從而創建一個特定型態的别名。

    • 這在設計模板類別或模板函數時特别有用,可以提高程式码的可讀性和便利性。例如:

    template <typename T>
    class MyClass {
    public:
        using VecType = std::vector<T>;
    
        void doSomething() {
            VecType myVector;
            // ...
        }
    };
    • 在此例,VecType 是一個依賴於模板參數 Tstd::vector 型態的别名。

    • MyClass 的成員函数 doSomething 中,可直接使用 VecType 而不是 std::vector<T>

全局特化與局部特化

  • 在 C++ 模板中,全局特化(Full Specialization)和局部特化(Partial Specialization)是用於自定義模板行為的技術。其允許為特定型態或一組型態提供特定的模板實現。

  • 這些特化技術在設計通用程式庫與框架時特别有用,因为它们可以優化與调整模板在特定情况下的行為。

  • 全局特化(Full Specialization)

    • 全局特化是指為模板定義一個特定型態的完全特定版本。

    • 在全局特化中,模板的所有參數都被具體的型態替换。意指特化的版本將僅用於這些具體的型態。

    • 全局特化適用於當所有模板參數都被特定类型替换时。

    • 全局特化一般用於改變模板在特定行態下的行為。

    template <typename T>
    void print(T value) {
        std::cout << value << std::endl;
    }
    
    // 可為 int 型態提供一個全局特化版本:
    template <>
    void print<int>(int value) {
        std::cout << "Integer: " << value << std::endl;
    }
    // 在這個例子中,當 print 被呼叫且型態為 int 時,會使用特化的版本。
  • 局部特化(Partial Specialization)

    • 局部特化是指為『類別模板』提供一個特化版本,其中一部分模板參數被具體型態替換,而其他參數仍然保持泛型。

    • 允許對模板的特定應用提供更特定的行為。

    • 局部特化则適用於只有部分模板參數被替换的情况。

    • 需要注意的是,C++ 標準只允許對『類別模板』進行局部特化,而不允許對函数模板進行『局部特化』。

    template <typename T, typename U>
    class MyPair {
        T first;
        U second;
        // ...
    };
    
    // 可為 T 為 int 的情況提供一個局部特化版本:
    
    template <typename U>
    class MyPair<int, U> {
        int first;
        U second;
        // 特定於 int 的實現
    };
    
    
    // 在這個例子中,當 MyPair 的第一個型態參數是 int 時,會使用此局部特化的版本。

可變參數模板與『模板模板參數』

  • 可變參數模板

    • 可變參數模板允許定義『接受任意數量和型態』的模板參數的模板。

    • 透過在模板參數列表中使用『省略号(...)』來實現的,其表明可以接受任意數量的模板參數。

    • 可變參數模板廣泛用於創建通用的函数和類別,特别是在需要處理不確定數量的型態時。

    template <typename... Args>
    void print(Args... args) {
        (std::cout << ... << args) << std::endl;  // C++17 折叠表達式
    }
    • 在此例子中,print 函數可以接受任意數量和型態的參數,並將其輸出。
  • 模板模板參數(Template Template Parameters)

    • 模板模板參數是指在模板定義中,參數本身就是一個模板。

    • 其允許編寫更靈活的程式碼,因可将模板作为參數傳遞给另一個模板。

    • 模板模板參數常用於設計需要泛型容器型態作为参数的類別與函數。

    template <template <typename...> class Container, typename T>
    class MyClass {
        Container<T> data;
        // ...
    };
    • 在此例子中,MyClass 為一個模板,其接受兩個參數:一個為模板 Container(本身為一個模板),另一個是普通的型態 T

    • MyClass 可與各種不同的容器模板(如 std::vectorstd::list 等)一起使用。

  • 结合使用

    • 這兩種特性可以结合使用,以提供更大的靈活性。例如,你可以創建一個接受任意数量的『模板模板参数』的可變參數模板:

      template <template <typename> class... Containers>
      class CombinedClass {
          // 结合多個容器的功能
      };
      • 此例中,CombinedClass 可以接受任意數量的模板作爲參數,每個模板都是一個接受單一型態參数的容器模板。