C++ 模板

dutrmp19發表於2024-11-17

C++ 模板

  • Created: 2024-03-24T20:24+08:00
  • Published: 2024-11-17T16:37+08:00
  • Categories: CPP

目錄
  • 偏特化
  • 類别範本
  • 函式模板
  • 形參包
  • Question
    • 我記不住模板的語法,尤其是偏特化的語法,怎麼辦?
  • 面試問題
        • 利用類别範本和函式模板實現編譯器計算斐波那契數列
        • 模板的宣告和定義為什麼不能分開寫,要想分開寫該怎麼做

在模板中,我們可以宣告三種型別的形參(Parameters),分別是:

  1. 非型別模板形參(Non-type Template Parameters)
  2. 型別模板形參(Type Template Parameters)
  3. 模板模板形參(Template Template Parameters)

——C++模板超程式設計(零):前言 - 知乎 的筆記,但是原文有些地方還是不太明白。

也就是說,定義模板的時候,引數不一定要寫作 typename T,還可以直接寫 int 之類的,如 my_template<5>()

偏特化

偏特化的意思是:當傳入的型別引數為 T 時候,我希望有一個自己重寫的實現,如:

#include <iostream>
template <typename T>
void print(T x)
{
    std::cout << x << std::endl;
}

template <>
void print<int>(int x)
{
    std::cout << "print(" << x << ")" << std::endl;
}

template <>
void print(double x)
{
    std::cout << "print(" << x << ")" << std::endl;
}

int main()
{
    print(1);
    print(1.1);
}

類别範本

類别範本基礎的用法略過,類别範本 C++11 不支援引數推導,所以使用時候一定要提供模板引數。
它的神奇之處在於:

  1. 支援偏特化
  2. 支援引用的偏特化,引用型別偏特化的語法也是獨樹一格:
// example from https://zhuanlan.zhihu.com/p/384826036
template <typename T> struct is_reference      { static constexpr bool value = false; };    // #1
template <typename T> struct is_reference<T&>  { static constexpr bool value = true; };     // #2
template <typename T> struct is_reference<T&&> { static constexpr bool value = true; };     // #3

std::cout << is_reference<int>::value << std::endl;    // 0
std::cout << is_reference<int&>::value << std::endl;   // 1
std::cout << is_reference<int&&>::value << std::endl;  // 1

更多用法參考:C++模板超程式設計(三):從簡單案例中學習 - 知乎

函式模板

首先要破除一些關於函式模板的迷思:

  1. 函式模板的模板引數雖然可以透過函式的引數推導,但是也可以直接指定,像 foo<int>() 就是在直接指定,並非語法錯誤。
  2. 函式模板只支援全特化。所以特化的模板一定要依賴於主模板,語法是把 template<typename T, ...> 中尖括號 <>所有的模板引數都列出來,一個都不能少。

函式和函式模板可以放在一起過載,看起來很燒腦,
那麼發生了一次函式呼叫,如何確定使用哪一個函式呢?按照我的理解:

  1. 函式呼叫時候,如果提供了模板引數列表,則顯式例項化,如 foo<int>(1) 提供了 <int>
    1. 先查詢是否有特化的函式模板,類似這個格式:template<> foo<int>()
    2. 沒有就找函式主模板
  2. 如果沒有提供模板的引數列表,則說明需要根據函式引數推導型別
  3. 從全特化的模板中查詢符合條件的
  4. 找不到就用主模板

嚴謹表述參考:C++模板超程式設計(四):深入模板 - 知乎

template<typename T>
void bar(T v) {
    std::cout << "1 template param" << std::endl;
    return;
}

template<>
void bar(int v) {
    std::cout << "1 template param specialization" << std::endl;
    return;
}

template<typename T, typename U>
void bar(T v) {
    std::cout << "2 template param" << std::endl;
    return;
}

int main()
{
    bar<int, float>(1);     // 2 template param
    bar<float>(1);          // 1 template param
    bar(1);                 // 1 template param specialization
    return 0;
}

bar<int, float> 顯式提供了模板引數,故例項化第三個 bar
bar<float> 也顯式提供了模板引數,確定型別 T 為 float,並且沒有找到對應的特換版本的函式,故例項化第一個
bar(1) 只提供了一個函式引數,只需要檢查函式簽名,對於第三個模板,無法推匯出型別 U;使用第二個模板。

函式模板 | 現代 C++ 模板教程

形參包

形參包 (C++11 起) - cppreference.com

  • 語法:我的記憶方法是,把 ... 寫在型別和包名中間,這樣可以避免忘記寫 ...
    template<typename ...Args>
    print(const Args& ...args) {
      int _[]{ (std::cout << args << ' ' ,0)... };
    }
    
  • 包展開:和中文語法下省略號的用法一致

Question

我記不住模板的語法,尤其是偏特化的語法,怎麼辦?

以 remove_reference 為例:

template <typename T> // line 1
struct remove_ref
{
    typedef T type;
};

template <typename T> // line 1
struct remove_ref<T &>
{
    typedef T type;
};

讓我們忽略 template 那一行,可以得到如下程式碼:

struct remove_ref // primary template does not need args
{
    typedef T type;
};


struct remove_ref<T &&> // specialization
{
    typedef T type;
};
  1. 主模板不需要寫 <args...>
  2. specialization 寫上自己需要例項化的時候,對應主模板的形式,最後在第一行補上未知的型別引數

template 那一行表示的是,下面要用到的未知的型別引數。

面試問題

利用類别範本和函式模板實現編譯器計算斐波那契數列

./template-fibo.cpp

注意:類别範本成員變數要用 static const 修飾

  • static 允許 fib<5>::value 這樣形式訪問
  • const static 允許編譯器進行計算

模板的宣告和定義為什麼不能分開寫,要想分開寫該怎麼做

因為是 C++ 是分離式編譯,不會遞迴查詢自己需要的函式。

// foo.h
template<typename T>
void foo(T a);

// foo.cpp
template<typename T>
void foo(T a) {
    std::cout << a << std::endl;
}

// main.cpp
#include"foo.h"
int main() {
    foo<int>();
}

編譯到 main.cppfoo<int> 時候,main.o 裡會有一個 call foo_int 的指令,等待連結的時候補上。
但是編譯到 foo.cpp 的時候,他不知道 main.o 裡面呼叫了 foo<int>,所以不會例項化 foo_int 這個函式。

可以直接把 foo.hfoo.cpp 合併到一個檔案解決這個問題,叫做 hpp
stackoverflow 上有兩種方法

  1. foo.h 最後加上一行 #include"foo.cpp",也就是合併到一塊寫。我看來不是從根本上解決這個問題。

  2. 問題的關鍵是,分離式編譯的時候,如何讓 foo.cpp 得知需要例項化 foo<int>,修改為:

    // foo.cpp
    template<typename T>
    void foo(T a) {
        std::cout << a << std::endl;
    }
    
    template void foo<int>; // tell the compiler to instantiation this function
    

相關文章