【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++模版元编程、设计模式
这里记录的只是入门相关的知识,后期如果还有相关内容的扩充,会继续更新。

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