【C++基礎】泛化編程之template(模版基礎)

前言

之前本科學習C++的時候,感覺自己還是對C++的知識有一點淺顯的認識,稍微深層一點的理解還有一些欠缺。
在我看STL源碼解析的時候,由於C++基礎不紮實,造成阻礙。因此,在邊刷題的時候,就邊進行C++的學習和複習。其實在用STL的時候,就有一點很好奇那就是爲什麼vector支持多種類型的定義,比如int、int_64、double等等,這些類型的定義是通過什麼實現的。這個就需要理解C++的基礎,泛化。
自己的碎碎念:
1)其實之前在複習並且深入理解C++的時候,也有看過相關視頻,我就是找的C++基礎班、進階班這種課程,採用跟着視頻從頭學到尾的方式,每次學習的時候比較煎熬,視頻很少有看完的。
2)後來逐漸改成,在學習其他內容的時候,看到不懂的地方,帶着問題和好奇的方式去學C++基礎知識。這樣在學習過程中會有一種恍然大悟的感覺,學習起來就不那麼枯燥了。如果大家有遇到我之前學習覺得學習很枯燥的時候,不妨試試帶着問題和好奇去學習~
3)帶着問題去學習話,不用擔心學習到知識不全面,因爲你在解決的這個問題的時候,會發現你其他不熟悉的點,然後下一個就去解決其他不熟悉的問題。就像一個網慢慢鋪開的那種,越擴越大,積累的多了,知識點就全了。

一、什麼是模版&泛型編程

1、概念理解:
1)動態綁定:把編譯時間的事情放到運行的時候去做(eg 多態:提供可擴展 高可擴展性)
2)模版元編程:把運行時間的事情放在編譯時候(高性能)
在運行的時候能夠動態實現的過程,挪到編譯的時候去實現,用編譯的時間去換取運行的時間,那麼運行的時間就快了。
在編譯的時候,更能保證語法安全。並且在編譯的時候可以幫你生成代碼,這個就是模版。

2、模版入門:
1)模版是泛化的基礎,泛型編程即以一種獨立於任何特定類型的編程。
C++在定義變量的時候都需要考慮變量的類型。那泛型編程即字面意思,即不再針對某一類型進行編程,對於任何類型都使用,更關注於算法。
這也就對應前面比較好奇的點,vector< int >、vector< double >的實現。
同時,在看STL源碼解析的時候,好奇 迭代器的實現,這個就是泛型編程的例子,使用了模版的概念。
2)可以用來定義模版函數和模版類

二、函數模版

1、函數模版的定義

template <class type>
ret-type func-name(parameter list)

2、函數模版的例子
相關介紹和注意點可以參考代碼註釋

//函數模版的實現
#include <iostream>
#include <stdio.h>
#include <string>
using namespace std;

//涉及內聯函數
template <typename T>
inline T const& Max(T const& a, T const& b)
{
    //在返回參數類型的時候,最好返回的是引用類型
    //這樣可以避免不必要的 複製構造函數
    return a < b ? b : a;
}

int main(int argc, const char * argv[]) {
    int i = 39;
    int j = 20;
    //此時在執行的時候,編譯器會尋在對應的Max<int,int>方法
    //沒有找到對應的方法,但是匹配到了Max(T const& a, T const& b)
    //因此編譯器在編譯的時候將 template<templatename T>,T替換爲int
    //因此此時變爲了Max<int,int>,這裏的int是根據入參i,j的類型決定的
    cout<<"Max(i,j)"<<Max(i,j)<<endl;
    double f1 = 20;
    double f2 = 30;
    //當入參爲浮點類型的時候,處理過程也是一樣的
    //在編譯的時候,Max此時被替換爲Max<double, double>
    cout<<"Max(f1, f2)"<<Max(f1, f2)<<endl;
    string str1 = "Hello";
    string str2 = "world";
    cout<<"Max(f1, f2)"<<Max(str1, str2)<<endl;
    std::cout << "Hello, World!\n";
    return 0;
}

模版不等同於宏展開
todo:內聯函數介紹

二、類模版

1、聲明

template <class T1, class T2>
class A{
    T1 data1;
    T2 data2;
}

2、編譯展開
1)模版不等同於宏展開
C++程序執行的過程:
(1)cpp文件 -> (2)預處理 -> (3)編譯 -> 等等
宏展開:

  • 發生時間:(1)cpp文件 -> (2)預處理 的過程
  • 實質:類似與文本的替換

模版:

  • 發生時間:(3)編譯 的時候展開
  • 實質:非文本替換,有實際的意義,本質是一種編程語言
  • 編譯:編譯器在編譯的時候,會生成一個二叉樹AST,在有模版的時候,會順着二叉樹不斷的進行實例化,展開。模版可以實現遞歸的功能,但同樣有最大遞歸深度。如果在執行的過程中,定義了模版函數或者模版類,但是沒有用它,此時不會在編譯階段進行展開。

2)模版優缺點
存在的問題:
1)由於模版是在編譯是進行展開的,所以會導致編譯時間極大增加。
2)可讀性差
優點
1)用編譯時間替換運行時間。
2)不用考慮類型

2、類模版的實現例子
1)首先實現一個僅能支持double類型的vector類(簡易版,STL裏面的vector源碼可以參考–專欄STL源碼解析)

#include <iostream>
#include <stdio.h>
using namespace std;
//類模版的聲明
/*
template <class T1, class T2>
class A{
    T1 data1;
    T2 data2;
}
 */

/**
    實現vector不再關注類型的方法
 */
class Vector{
private:
    //pointer to elements
    double* elem;
    //number of elements
    size_t sz; //todo:size_t
    
public:
    //構造函數
    explicit Vector(size_t size)
    :sz{size},
    elem(new double[sz])
    {
        
    }
    
    //運算符重載
    double& operator[](int i){
        return elem[i];
    }
    size_t size(){
        return sz;
    }
    Vector(const Vector& other);
    ~Vector() {delete[] elem;}
};

Vector::Vector(const Vector& other)
    :sz(size()),
    elem(new double[sz]){
        for(int i = 0; i != sz; i++){
            elem[i] = other.elem[i];
        }
}

int main(int argc, const char * argv[]) {
    // insert code here...
    std::cout << "Hello, World!\n";
    return 0;
}

todo:explicit、size_t的介紹
2)加入類模版,使得vector類能夠支持各種類型

#include <iostream>
#include <string>
using namespace std;
template <typename T>
class Vector{
private:
    size_t sz;
    T* elem; //元素類型定義爲模版類型,然後將後面的double都替換爲T
public:
    explicit Vector(size_t size)
    :sz{size},
     elem(new T[s])
    {
    }
    
    T& operator[](int i){
        return elem[i];
    }
    
    Vector(const Vector& other);
    ~Vector(){delete [] elem;}
};

//如果把方法放在外面
template<typename T>
const T& Vector<T>::operator[](int i) const{
    if( i < 0 || i >= size())
        throw out_of_range(**);
    return elem[i];
}

template<typename T>
Vector<T>::Vector(const Vector& other)
    :sz{size()},
    elem(new T[s])
{
    for(int i = 0; i != sz; i++){
        elem[i] = other.elem[i];
    }
}

int main(int argc, const char * argv[]) {
    // insert code here...
    Vector<int> intVec;
    Vector<std::string> strVec;
    std::cout << "Hello, World!\n";
    return 0;
}

三、全特化與偏特化

1、全特化
1)概念:
–通過全特化一個模版,可以對一個特定參數集合自定義當前模版,類模版和函數模版都可以全特化。全特化的模版參數列表應該是空的,並且應該給出“模版實參”列表。
2)模版
全特化類模版

//全特化模版
template<>
class A<int, double>{
    int data1;
    double data2;
}

全特化函數模版

//全特化函數模版
template<>
int max(const int lhs, const int rhs){
    return lhs>rhs? lsh:rhs;
}

i)簡單來說,全特化模版就是,編譯器在實例化或者展開的時候,去匹配對應類型的,比如全模塊化A <int, double>,那麼在實例化的時候,如果入參就是int,double,編譯器會比配到全模塊化A <int, double>,然後執行裏面的邏輯,此時就不會進入到A <T, T>的邏輯。

ii)這個過程就像 if {} else {}語句,if(num1.type == int && num2.type == double)的時候,進入到全特化模版的分支,執行一個邏輯。如果沒有命中這個if條件的話,就去匹配 A <T, T>。

iii)類模版全特化時,在類名後給出了“模版實參”。但是函數模版的函數名後沒有給出“模版實參”。這是因爲編譯器根據int max(const int,const int)的函數簽名可以推導出來它是T max(const T,const T)的特化。
3)核心思想
全特化實現了一個邏輯分支的選擇,但是在實現過程中沒有用到if()else{} 這種面向過程的方式。
4)函數模版特化歧義
如果函數模版沒有指定“模版實參”的話,有時會產生歧義。

template <class T>
void f() {T d;}

template <>
void f() {int d;}
//此時就會又歧義,編譯器不知道f()是從f<T>()中的哪個特化來的。

//改爲
template <>
void f<int>() {int d;}

2、偏特化
1)概念:
類似於全特化,偏特化也是爲了給自定義一個參數集合的模版。但是偏特化後的模版需要進一步實例化才能形成確定的簽名。值得注意的是函數模版不允許偏特化。偏特化也是以template來聲明的,需要給去剩餘的“模版型參”和必要的“模版實參”。

//此時一個參數的類型是確定的爲int,另外一個參數的類型可以爲任意
template <class T2>
class A<int, T2>{
};

大多數情況下,函數模版重載就可以完成偏特化的需求,一個例子外便是std命名空間。std是一個特殊的命名空間,用戶可以特化其中的模版,但不允許添加模版。

3、學習工具
todo:可以採用clang++,展開模版代碼,看編譯器是什麼展開模版的

四、元編程

1、概念
1)元編程就是一個接受類型並且返回類型的函數,這個函數就是元函數
ps:偏特化(分支語句)和遞歸(循環語句),我們可以隨意自如的處理類型
簡單來說,就是之前編程的時候,我們採用的返回類型都是一個類型值,而不是一個類型,元編程就是返回一個類型,操作類型

template <typename T>
class Vector{
public:
	//此時using的用法:value_type就是用來接收T傳過來的類型
	//todo:value_type就是一個通用方法(convention)
	using value_type = T;
}template <typename C>
//新定義一個type:Element_type,Element_type就是typename C裏面的元素的類型
using Element_type = typename C::value_type;

template <typename Container>
void algo(Container& C){
	//元編程不用關心具體的類型,通過類型推導出類型
	//Element_type<Container>  展開成了Container::value_type
	//如果是Vector<int>,則此時實例化之後,value_type = int
	Vector<Element_type<Container>> vec;
}

2、模版相關
1)alias:using的用法

template<typename Key, typename Value>
class Map{
};

//元編程的應用
//條件1):如果說我們在定義map的時候,想一直用 map<string, Value>的類型,就可以採用下面的方式
//新定義一個類型String_map
template <typename Value>
using String_map = Map<string, Value>;
//此時String_map就是滿足條件1)的類型
String_map<int> m; //m is a Map<string,int>

In header<cstddef>:
//比如size_t,實際上size的定義如下,定義在頭文件cstddef中
using size_t = unsigned int;

2)const和constexpr
i)constexpr:在編譯時就可以被編譯器確定的一個常量

const int dmv = 17;  //dmv is a named constant
int var = 17;
constexpr double max1 = 1.4 * square(dmv); //此時代碼執行是沒有問題。在編譯的時候,max1已經被賦值爲1.4 * square(17)了,已經是具體的值了
constexpr double max2 = 1.4 * square(var); //error:var is not a constant expression

五、模版高級特性

1、沒有任何overhead的創建一個模版Buffer類

template<typename T, int N>
struct Buffer{
	using value_type = T;
	constexpr int size() {return N;}
	T[N];
};

參考資料:
這篇博客應該算:張嘉星老師的C++特訓班7.1-7.5的課堂筆記
老師推薦的書籍:C++Templates、C++模版元編程、設計模式
這裏記錄的只是入門相關的知識,後期如果還有相關內容的擴充,會繼續更新。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章