講義七

模板(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<> 內有多少模板參數;主要是看函數模板內的函數名稱之後的參數數量。

      EX:

      template <typename T, typename U, typename W>
      T add(T a, U b, W c) { return a + b + c; }
      • 它有 3 個模板參數 (T, U, W)

      • 但在呼叫時,C++ 不會先看這些 template 參數數量

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

      • 它只看:

        函數名稱:add

        函數參數:add(a, b, c) → 三個參數

      • 模板參數推導 (template argument deduction)

        讓 C++ 自動 deduce T, U, W:

        • auto r = add(1, 2.0, 3);

          C++ 自動推論:

          T = int

          U = double

          W = int

        • 所以:

          • 呼叫時 根本不需要知道 template 裡面有幾個參數

          • 呼叫時只需要 看函數參數列(function parameter list)

    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, 13與int一致。
      
      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)』可以是一個整數型態,或是一個指向物件或函數型態的『指標』或『左值參考』。

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

      • 因為模板實例化在 編譯期,但物件的生命期若是在 執行期才決定,編譯器就無法保證:

        • 這個物件會存在

        • 它有固定的位址

        • 它能成為編譯期常量

        int func() {
            int a = 5;
            X<a> obj;  // 不合法。a 是在 執行期 才建立,它的位址不固定,不符合「編譯期常數」的要求
        }
    • 無法使用一個普通(非 static)局部變數,或動態物件作為指標或參考『非型態參數(nontype parameter)』的實參。

    • 非型態模板參數要求:

      • 編譯期可得知

      • 值具有固定記憶體位址或大小

      • 推薦:constconstexprstatic

      • 字面量如 "hi" → 其陣列長度為編譯期常數 → 可以當模板參數

      • 『指標』或『參考』作為模板參數 → 被綁定或指向的物件必須具有「靜態生命期」

      • 動態物件(new 產生的物件位址在『執行期』決定)或局部變數物件不符合要求,故無法使用inlineconstexpr 的函數模板

    • 函數模板可以宣告為 inlineconstexpr(允許編譯期求值,保證函數在條件允許下於編譯期計算)。

      • 為什麼需要 inline

        • 因為模板是『按需求實例化』,如果在多個翻譯(編譯)單元(translation units)使用相同模板實例,編譯器會在每個翻譯(編譯)單元會產生相同的函數實體,進而導致連結時發生 『multiple definition』 錯誤。

        • 可允許同一個函數模板實體存在於多個翻譯單元,並允許連結器把它們視為同一個定義(ODR-friendly

        • 若模板定義放在 header 檔且同時被多個 cpp 檔所使用,則建議使用 inline

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

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

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

    • 為某些特定型態提供不同的實作,但仍可保持『泛型函數模板』的介面不變。

    • 完全特化語法固定為:

      template <>
      ReturnType function_name<SpecificType>(arguments) { ... }
    template <typename T>
    T abs_value(T x) {
        return x < 0 ? -x : x;
    }
    
    // 如果 T = int, double → 正常
    // 但如果 T = std::complex → 不合法(無 < 運算符)
    // 此時我們希望提供特定型態的版本:
    template <>
    std::complex<double> abs_value(std::complex<double> x) {
        return std::abs(x);
    }
    template <>
    void swap<double>(double& a, double& b) {
        // 特殊處理 double 型態的 swap
    }
    • 注意事項:

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

        template <typename T>
        T add(T a, T b) {
            return a + b;
        }
        
        add(1, 2);      // T 被推斷為 int
        add(1.0, 2.5);  // T 被推斷為 double
        
        add(1, 2.5);  // 錯誤:T 要同時是 int 與 double → 無法推斷
        // 必須手動指定:
        add<double>(1, 2.5);  // 正確
        
        // 從回傳值無法推斷
        template <typename T>
        T make_value();
        
        auto x = make_value();  // 錯誤:無法推斷 T
        int x = make_value<int>();  // 正確
        
        
      • 過度泛化:雖然函數模板可以提高程式碼的重用性,但過度泛化可能會導致程式碼難以理解和維護。故適當且合理地使用模板對於寫出高品質的程式非常重要。

        // 過度泛化
        template <typename T>
        bool equal(T a, T b) {
            return a == b;
        }
        • 所有型態都允許比較,但某些型態根本不能定義良好的等號

        • 可能產生編譯錯誤或語意錯誤

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

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

類別模板

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

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

  • 定義類別模板:

    • 類別模板的定義以 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 <typename T>
      class MyClass {
      public:
          void display() {
              std::cout << "Display function with type T" << std::endl;
          }
      };
      template <typename T>
      class MyClass {
      public:
          void display(); // 僅宣告
      };
      
      // 成員函數定義在類別外部
      template <typename T>
      void MyClass<T>::display() {
          std::cout << "Display function with type T" << std::endl;
      }
  • 類別模板的『特化』:

    • 對於某些特定的資料型態,可提供特殊的類別實現,這就是所謂的『類別模板特化』。

    • 在特化中,可為特定的模板參數提供不同的實作。例如:

      template <>
      class Box<char> {
      public:
          char contents;
          // 特殊的實現
      };
      template <>
      class MyClass<int> {
      public:
          void display() {
              std::cout << "Specialized display function for int" << std::endl;
          }
      };
      // 類別模板的『局部特化』
      template <typename T1, typename T2>
      class MyClass {
      public:
          void display() {
              std::cout << "Generic display" << std::endl;
          }
      };
      
      
      // 『局部特化』版本: 允許根據部分模板參數進行特化,但只適用於類別模板(函數模板不支援局部特化)。
      template <typename T>
      class MyClass<T, int> {
      public:
          void display() {
              std::cout << "Partially specialized display for second type as int" << std::endl;
          }
      };
  • 注意事項:

    • 型別安全:類別模板增加了程式碼的型態安全,因編譯器會在編譯時檢查型別錯誤。

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

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

  • 模板用於『標準模板程式庫(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 來告訴編譯器,特定的標識符表示為一個『型態』

    • 這種情況通常發生在『模板內部』,尤其是在處理『嵌套相依型態』時。

      • T::SubType 就是一個典型例子:

        • 是一個型態

        • 是一個靜態成員變數或常量

      • 而編譯器在看到模板時 還不知道 T 是什麼,因此無法判斷 T::SubType 應被當成『型態解析』或是『值解析』?

      template <typename T>
      class MyClass {
          typename T::SubType value;  // 使用 typename 指定 T::SubType 是一个类型
          // ...
      };
      • T::SubType 可能是一個相依於模板参数 T 的型態。

      • 由於編譯器在處理模板時無法確定 T::SubType 是否應視為型態,故 typename 關鍵字在此處是必要的,以避免歧義。

      • 只要型態名稱 依賴模板參數 就一定要寫 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)。

      • 虛函數的動態繫結需要求「確定的函數簽名」

      • vtable 需要在編譯期間(compile time)就要確定每個虛函數的「確切函數簽名」

        • vtable要求:

          • 每個虛函數意指唯一的函數指標值(函數記憶體位址)

          • 在編譯期就要加入 vtable

      • 但『函數模板』並沒有固定簽名。

      • 函數模板並非真正的函數:

        • 一個「函數產生器」,必須由模板參數(T)實例化後,才會變成真正的函數
      • 那「類別模板可以有虛函數」可以嗎?

        • 可以。

          template <typename T>
          class Base {
          public:
              virtual void func() {}   //  合法:虛函數本身不是模板
          };
    • 成員函數模板的使用提高程式碼的通用性,但也可能增加了複雜性,特别是在涉及模板嵌套與特化时。

模板『顯式』實例化與宣告

  • 一般來說,模板的定義必須放在可視的範圍(通常放在標頭檔 .h 中),否則編譯器在看到模板使用處時不知該如何產生實際程式碼。

  • 故在大型專案中會導致:

    1. 編譯時間拉長:幾乎每個使用模板的編譯單元,都會重新編譯模板定義。

    2. 程式碼膨脹:可能在不同編譯單元中重複產生相同的模板實例。

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

  • 模板顯式實例化(explicit instantiation definition)

    • 在 C++ 中,當使用模板定義一個函数或類別时,編譯器並不立即產生該模板的實際程式碼。相反,它會等到模板被實際使用時(例如,創建模板類別的物件或呼叫模板函數)時才會進行實例化。

      • 因此,會遇到某個位置欲『顯式』告訴編譯器在某個特定點產生模板的實例化程式码,此即為所谓的『模板顯式實例化』。
    • 有些模板非常龐大或內部實作複雜,編譯器在自動實例化時,需要做很多工作。

      • 如果在程式初期就能知道將來要用到哪幾種特定型別,可以直接在某個翻譯單元中顯式實例化,讓編譯器『提早』產生那些實體並完成編譯。

      • 這樣就不用在使用處再進行實例化,其可以減少重複工作並縮短整體編譯的時間。

    • 故顯式實例化能幫助你在單一翻譯單元集中生成所需的實體,減少在其他翻譯單元中重複定義的風險。

    • 顯式實例化的語法:

      • 顯式實例化必須出現在「模板完整定義對編譯器可見」之後(否則會出現編譯錯誤),並在模板定義前加上 template 關键字與模板實例的具體型態。例如:
    // MyClass.h
    template <typename T>
    class MyClass;   // 只有宣告,沒有定義
    
    
    // MyClass.cpp
    #include "MyClass.h"
    
    template class MyClass<int>;   // 錯誤
    // MyClass.h
    template <typename T>
    class MyClass {
    public:
        void foo();
    };
    
    
    // MyClass.cpp
    #include "MyClass.h"
    
    template <typename T>
    void MyClass<T>::foo() {}
    
    // 模板定義「完整可見」
    template class MyClass<int>;
    template <typename T>
    void myFunction(T);
    
    
    template void myFunction<int>(int); // 錯誤:沒有定義,無法實例化
    template <typename T>
    void myFunction(T x) {}
    
    template void myFunction<int>(int); // OK
  • 模板宣告

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

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

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

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

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

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

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

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

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

  • 使用 using 定義『模板别名』

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

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

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

    • typedef 無法為模板建立別名。

    template <typename T>
    using Vec = std::vector<T>;
    
    Vec<int> intVector; // 等同於 std::vector<int>,形成一個具體型別(type formation),會觸發「隱式實例化」
  • 使用 using 顯式指定模板參數

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

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

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

    • VecType 在類別範圍內等價於 std::vector<T>

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

    • 由於 VecType 僅為型別層級的別名,此定義不會觸發任何模板實例化。

  • 嵌套模板的最佳用法

    template <typename T>
    using VecMap = std::map<std::string, std::vector<T>>;
    
    VecMap<int> x;      // std::map<string, vector<int>>
    VecMap<double> y;   // std::map<string, vector<double>>
    
    // 沒有 using 時,你需要寫出一大串複雜語法:
    std::map<std::string, std::vector<int>> x;
    • 若未來改成:std::map<std::string, std::deque<T>>,只需改一行,後面使用的部分不需修改。

      template <typename T>
      using VecMap = std::map<std::string, std::deque<T>>;
    • 對於深度嵌套的模板型別,應優先使用 using 建立模板別名,以封裝語意並降低閱讀與維護成本。

全局特化與局部特化(偏特化)

  • 在 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 T>
    struct Traits {
        static constexpr bool is_pointer = false;
    };
    
    // 局部特化:任何指標型別
    template <typename T>
    struct Traits<T*> {
        static constexpr bool is_pointer = true;
    };
  • 函數模板僅支援全局特化,不支援局部特化,其相近效果通常以函數重載實現。

  • QuantLib 用 partial specialization 分類型別,用 full specialization 精準鎖定型別。

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

  • 可變參數模板(Variadic Templates)

    • 可變參數模板允許模板參數列表中包含 零個、任意個數 的型別或非型別參數。

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

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

      template <typename ... T>
      void myfunct1(T ... args)     // args: 可變形式參數,代表一包參數 // T ... : T稱為 『可變參數型態』
      {
        std::cout << sizeof...(args) << std::endl; 
        // std::cout << sizeof...(T) << std::endl;
      }
      
      myfunct1();                          // 0
      myfunct1(10, 20);                    // 2
      myfunct1(10, 2.2, "abc", 68);        // 4
    template <typename T, typename ... U>
    void myfunct2(const T& firstarg, const U& ... otherargs)
    {
      std::cout << sizeof... (firstarg) << std::endl;   // 錯誤:sizeof...只能用在一包型態或一包形參
      std::cout << sizeof... (otherargs) << std::endl;
    }
    
    
    myfunct2();      // 錯誤
    myfunct2(10);    // 0      
    myfunct2(10, "abc", 12.5);  // 2  
  • 參數包的展開(解包)

    • 一般都是用遞迴函數的方式展開參數包。

    • 一個 『參數包展開函數』 與 一個『同名遞迴終止函數』。

    • 這種「遞迴 + base case」寫法是 C++11/14 時代的典型技巧。

      #include <iostream>
      
      void myfunc2()
      {
          std::cout << "參數包展開執行了遞迴終止函數 myfunc2()" << std::endl;
      }
      
      template <typename T, typename... U>
      void myfunc2(const T& firstarg, const U&... otherargs)
      {
          std::cout << "收到的參數值為: " << firstarg << std::endl;
          myfunc2(otherargs...);   // 遞迴展開參數包
      }
      
      int main()
      {
          myfunc2(42, 3.14, "Hello", 'A');
      }
      
      
      
      // C++ 17之後的寫法:折叠表達式
      template <typename... Args>
      void myfunc2(Args&&... args)
      {
          (std::cout << ... << args) << std::endl;
      }
    • template <typename... Args>
      void print(Args... args) {
          (std::cout << ... << args) << std::endl;  // C++17 折叠表達式
      }
      • template <typename... Args>:宣告這是可變參數模板,Args... 表示任意數量、任意型別的參數。
      • 呼叫 print(1, "hello", 3.14, std::string("C++")); 時,編譯器會根據形參推導(template argument deduction)出 Args 分別為 <int, const char*, double, std::string>
      • (std::cout << ... << args) 為 C++17 引入的「折疊表達式」,可將左邊的運算(std::cout <<)對多個 args 進行逐項展開
  • 模板模板參數(Template Template Parameters)

    • 模板的參數本身就是「一個模板」。

    • 語法:

      template <template <class...> class Container, typename T>
      class Wrapper { ... };
      • template <class...>

        • Container 是一個帶有「型別參數包」的模板(例如 vector<T>list<T>)。

        • Container 是一個模板類別,其模板參數列表接受「任意多個 class 型參」

      • class Container

        • Container 是一個「模板名稱」,不是型別。
      • Container 必須可以被實例化為:

        • Container<T>,因此,你可以傳入:std::vectorstd::list,任何自定義的 template <typename...> class MyContainer
      • 用途:

        - 對容器類型進行抽象(Vector vs List vs 自訂容器)

        - 寫出高度泛用的資料結構

        - 支援政策模式(Policy-based design)

        • 將類別的行為拆成「多個獨立的政策(Policy Classes)」

        • 再透過 模板參數(尤其是模板模板參數) 將這些政策組合起來

        • 在「編譯期」決定類別的行為,而非「執行期」

    • 模板模板參數則是指「把一個模板當作參數」傳遞給另一個模板」。它允許我們在定義一個模板時,要求「呼叫者必須提供另一個符合指定介面的模板」。

    • 常見用法之一是為了抽象化容器類型,例如想寫一個模板類別,可以接受「任何模板形式的容器(像 std::vectorstd::list,或自定義的 MyContainer)」並用於儲存某種型別 T 的資料。

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

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

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

    #include <vector>
    #include <list>
    #include <iostream>
    
    
    // template <template <class...> class Container, class T>
    //                    ^^^^^^^^ 這裡是 "模板模板參數" 的關鍵字
    //          ↑————— 參數本身就是 "一個帶有型別參數可變數量" 的模板
    template <template <class...> class Container, typename T>
    class Wrapper {
    public:
        // 以提供的模板 'Container' 與型別 'T' 為基底,來實例化新的容器
        Container<T> data;
    
        Wrapper() = default;
    
        void addElement(const T& value) {
            data.push_back(value);
        }
    };
    
    
    int main() {
        // 以 std::vector 為模板傳入
        Wrapper<std::vector, int> wv;
        wv.addElement(10);
        wv.addElement(20);
    
        // 以 std::list 為模板傳入
        Wrapper<std::list, int> wl;
        wl.addElement(30);
        wl.addElement(40);
    
        // ...後續程式碼
        return 0;
    }
    • template <class...> class Container 表示此參數 Container 本身是一個模板,而且它的模板參數列表是 class...,表示可變數量的 class 型別(類似 std::vector, std::list, 等等)。
    • typename T 表示要儲存的資料型別。
  • 结合使用

    • 可將「模板模板參數」與「可變參數模板」結合使用。例如,想要允許容器本身能接受任意多個型別參數(譬如 std::map<Key, T, Compare, Alloc> 之類)。如此便能支援更多複雜的容器模板形式。可以寫成:

      template <template <typename...> class Container, typename... Args>
      class FlexibleWrapper {
      public:
          FlexibleWrapper() = default;
      
          // 這裡用一個容器,容器的泛型參數就是 Args...
          Container<Args...> data;
  • 可變參數模板

    • 關鍵語法:typename... Args / class... Ts + ... 展開。

    • 用於:

      • 函式的參數個數與型別都可能不同時(如通用的 print 函式)。

      • 類別中需要封裝/儲存多種型別的結構(如 std::tuple)。

    • 重點:聚焦在「不定數量型別函式參數」。

  • 模板模板參數

    • 關鍵語法:template <class...> class Container(或非可變參數的版本)。

    • 用於:

      • 需要在模板中「再注入一個模板」以進行二次定義,例如要接受「一個可存放 T 的容器模板」。

      • 允許在設計階段,將「容器模板」或「Policy 類別模板(策略或是行為的封裝)」等抽象出來(與傳統的「繼承+虛函式」相比,模板注入(Policy 類別模板)方式可以在『編譯期』決定最終行為,省去執行期虛函數呼叫的額外成本,也更容易在不同實例之間做內聯或最佳化。)。

    • 重點:聚焦在「將一個模板當作參數傳遞給另一個模板」,以實現模板組合策略模式等設計。

  • 可變參數模板(Variadic Templates)解決「我想寫一個可以接受任意數量/型別的函式或類別」的需求。

  • 模板模板參數(Template Template Parameters)則解決「我想寫一個模板,需要把另一個模板(而非具體型別)傳進來」的需求。