C++ 模板
- Created: 2024-03-24T20:24+08:00
- Published: 2024-11-17T16:37+08:00
- Categories: CPP
- 偏特化
- 類别範本
- 函式模板
- 形參包
- Question
- 我記不住模板的語法,尤其是偏特化的語法,怎麼辦?
- 面試問題
- 利用類别範本和函式模板實現編譯器計算斐波那契數列
- 模板的宣告和定義為什麼不能分開寫,要想分開寫該怎麼做
在模板中,我們可以宣告三種型別的形參(Parameters),分別是:
- 非型別模板形參(Non-type Template Parameters)
- 型別模板形參(Type Template Parameters)
- 模板模板形參(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 不支援引數推導,所以使用時候一定要提供模板引數。
它的神奇之處在於:
- 支援偏特化
- 支援引用的偏特化,引用型別偏特化的語法也是獨樹一格:
// 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++模板超程式設計(三):從簡單案例中學習 - 知乎
函式模板
首先要破除一些關於函式模板的迷思:
- 函式模板的模板引數雖然可以透過函式的引數推導,但是也可以直接指定,像
foo<int>()
就是在直接指定,並非語法錯誤。 - 函式模板只支援全特化。所以特化的模板一定要依賴於主模板,語法是把
template<typename T, ...>
中尖括號<>
中所有的模板引數都列出來,一個都不能少。
函式和函式模板可以放在一起過載,看起來很燒腦,
那麼發生了一次函式呼叫,如何確定使用哪一個函式呢?按照我的理解:
- 函式呼叫時候,如果提供了模板引數列表,則顯式例項化,如
foo<int>(1)
提供了<int>
,- 先查詢是否有特化的函式模板,類似這個格式:
template<> foo<int>()
- 沒有就找函式主模板
- 先查詢是否有特化的函式模板,類似這個格式:
- 如果沒有提供模板的引數列表,則說明需要根據函式引數推導型別
- 從全特化的模板中查詢符合條件的
- 找不到就用主模板
嚴謹表述參考: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;
};
- 主模板不需要寫
<args...>
- 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.cpp
中 foo<int>
時候,main.o
裡會有一個 call foo_int
的指令,等待連結的時候補上。
但是編譯到 foo.cpp
的時候,他不知道 main.o
裡面呼叫了 foo<int>
,所以不會例項化 foo_int
這個函式。
可以直接把 foo.h
和 foo.cpp
合併到一個檔案解決這個問題,叫做 hpp
。
stackoverflow 上有兩種方法:
-
在
foo.h
最後加上一行#include"foo.cpp"
,也就是合併到一塊寫。我看來不是從根本上解決這個問題。 -
問題的關鍵是,分離式編譯的時候,如何讓
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