National Taiwan University
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try a Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

2

Local AI

--

Uedu Code

--

台灣大學 AQI 70 24°C PM2.5 17
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 18

模板

函式模板、類別模板與型別推導
Interaction實作 20 minute(s)

Learning Objectives

  • 說明為什麼需要模板:同一段邏輯不必為每種型別各寫一次
  • 用 template <typename T> 寫函式模板,說明編譯器如何從引數推導 T
  • 看懂型別推導衝突的錯誤,並用 <型別> 明確指定
  • 寫類別模板,並在類別外定義它的成員函式
  • 使用非型別模板參數,並說明 vector<int>、map<string, int> 就是類別模板的實例

為什麼需要模板

假設你要寫一個「回傳兩數中較大者」的函式。C++ 的每個參數都要寫型別,所以 int 寫一個、double 寫一個、string 再寫一個:

int    largerInt(int a, int b)          { return a > b ? a : b; }
double largerDouble(double a, double b) { return a > b ? a : b; }
string largerString(string a, string b) { return a > b ? a : b; }

三個函式除了型別,內容一模一樣。哪天發現邏輯有錯,要改三個地方,漏改一個就出問題。

模板(template)讓你把型別也當成參數:

template <typename T>
T larger(T a, T b) {
    return a > b ? a : b;
}

T 是型別參數,代表「呼叫時才決定的某個型別」。這段程式本身還不是函式,而是產生函式的樣板: 你寫 larger(3, 7) 時,編譯器看出 T 是 int,就照著模板產生一份 larger<int>;寫 larger(2.5, 1.5) 又產生一份 larger<double>。 這個過程叫實例化(instantiation),發生在編譯時,程式執行時不需要再判斷型別。

template <typename T> 也可以寫成 template <class T>,兩者在這裡意思完全相同;T 只是慣用的名字,取別的名字也可以。

一個模板,四種型別

前兩次呼叫的是為 int、double 分別手寫的函式。接下來四次都呼叫同一個模板 larger,注意每次新框架的名稱:larger<int>、larger<double>、larger<string>、larger<char>,這是編譯器依引數型別產生的四個不同函式。

型別推導與明確指定

呼叫函式模板時,編譯器會從引數的型別推導 T,這叫模板引數推導(template argument deduction)。 推導時不做任何型別轉換,每個用到 T 的參數都必須推導出同一個型別:

template <typename T>
T add(T a, T b) { return a + b; }

add(2, 3);        // T = int
add(1.5, 2.25);   // T = double
add(2, 0.5);      // 編譯錯誤:第一個引數推出 int,第二個推出 double,衝突

解決的方法有兩種:

  1. 明確指定型別:add<double>(2, 0.5)。指定之後就不用推導,2 會像一般函式呼叫一樣轉成 2.0
  2. 用兩個型別參數:template <typename T, typename U>,讓兩個參數各自推導

模板只有在被呼叫時才會依實際型別檢查。如果某個型別不支援模板裡用到的運算(例如對沒有定義 > 的類別呼叫 larger), 編譯器會在實例化的地方報錯,錯誤訊息常常很長,看的時候先找「required from here」附近指出的那一行呼叫。

推導、明確指定與兩個型別參數

注意每次呼叫的框架名稱:add<int>、add<double>。第 19、20 行用角括號指定 T,引數會先轉成那個型別再傳入,看 add<int>(7, 2.9) 裡的 b 變成了多少。show 有兩個型別參數,"one" 這種字串字面值推導出的是 const char*。

想一想:函式模板

1.

下面的程式能不能編譯?如果能,會印出什麼?

template <typename T>
T add(T a, T b) { return a + b; }

int main() {
    cout << add(2, 0.5);
}
2.

下面的程式會印出什麼?

template <typename T>
T twice(T x) { return x + x; }

int main() {
    cout << twice(3) << " " << twice(1.5) << " " << twice<int>(2.9);
}
3.

larger("apple", "banana")(larger 是前面的模板)有什麼問題?

第 1 次作答

類別模板

類別也可以寫成模板。前一課的 vector<int>、map<string, int> 就是標準函式庫裡的類別模板, 角括號裡的型別就是模板引數:

template <typename T>
class Box {
    T item;
public:
    Box(T value) : item(value) {}
    T open();                      // 只宣告
};

template <typename T>              // 在類別外定義成員函式:
T Box<T>::open() {                 // 要再寫一次 template,類別名稱寫成 Box<T>
    return item;
}

Box<int> a(42);                    // 使用時寫出型別
Box<string> b("hello");

幾個要點:

  • Box 本身不是型別,Box<int>、Box<string> 才是,而且是兩個不同的型別,不能互相指定
  • 成員函式寫在類別外面時,前面要加 template <typename T>,類別名稱寫成 Box<T>::
  • 模板的定義通常整個放在標頭檔(.h)裡,因為編譯器在每個使用的地方都要看到完整的定義才能實例化

C++17 起可以在建構時省略型別,寫成 Box b(42); 讓編譯器從建構子的引數推導(類別模板引數推導,CTAD)。 CodeLab 的直譯器還不支援這個寫法,本課的範例一律寫出 Box<int>。

類別模板 Box<T>

a 是 Box<int>、b 是 Box<string>,看右邊兩個物件的成員型別各不相同。呼叫成員函式時,框架名稱寫成 Box<int>::open 或 Box<string>::open:同一份在類別外定義的 open,依物件的型別各自實例化。

非型別模板參數

模板參數不一定是型別,也可以是一個編譯時就確定的整數(或其他常數),這叫非型別模板參數:

template <typename T, int N>
class RingBuffer {
    T data[N];          // N 在編譯時已知,可以當陣列大小
};

RingBuffer<int, 3> last3;      // 只保留最後 3 筆
RingBuffer<double, 7> week;    // 只保留最後 7 筆

RingBuffer<int, 3> 和 RingBuffer<int, 4> 是不同的型別。N 必須是常數運算式:寫 5、const int n = 5; 的 n 可以, 一般的變數 int n = 5; 不行,因為編譯器在編譯時不知道它的值。標準函式庫的 std::array<int, 5> 就是用這種參數決定大小。

RingBuffer:只保留最後 N 筆

last3 裡的陣列只有 3 格。加入第 4、5 筆時,next 繞回 0,新的值覆蓋最舊的資料。注意框架名稱 RingBuffer<int, 3>::add:N 是型別的一部分,函式裡的 N 直接就是 3。

想一想:類別模板

1.

類別模板 Box<T> 在類別裡宣告了 T open();,要在類別外面定義它,正確的寫法是哪一個?

2.

下面的程式會印出什麼?

template <int N>
int times(int x) { return x * N; }

int main() {
    cout << times<3>(5) + times<2>(1);
}
第 1 次作答

動手寫:通用的堆疊 Stack<T>

完成類別模板 Stack<T>:push 把元素放到最上面、pop 取出並回傳最上面的元素、empty 回傳是否為空。內部用 vector<T> 儲存,尾端就是「最上面」。main 同時用了 Stack<int> 和 Stack<string>。

你的輸出
預期輸出

重點回顧

  • 模板把型別當成參數,同一段邏輯只寫一次;編譯器在編譯時依實際型別產生各自的版本(實例化)
  • 函式模板的 T 由引數推導,推導時不做型別轉換;推導衝突時用 函式名<型別>(...) 明確指定,或改用兩個型別參數
  • 類別模板使用時要寫出型別,例如 Box<int>;Box<int> 和 Box<string> 是不同的型別
  • 在類別外定義成員函式要寫 template <typename T> 和 Box<T>::
  • 非型別模板參數是編譯時已知的常數,例如 RingBuffer<int, 3> 的 3,可以當陣列大小
  • vector<int>、list<string>、map<string, int> 都是標準函式庫的類別模板
這一課完成了!
看我的學習進度
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。