[C++系列 64. 模板深层次讨论

1. 非类型模板参数

模板参数分类类型形参与非类型形参。
类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
非类型形参:就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。

// 定义一个模板类型的静态数组
template<class T, size_t N = 10>
class Array
{
public:
    T& operator[](size_t index)
    {
        return _array[index];
    }
    
    const T& operator[](size_t index)const
    {
        return _array[index];
    }
    
    size_t Size()const
    {
        return _size;
    }
    
    bool Empty()const
    {
        return 0 == _size;
    }
    
private:
    T _array[N];	// 保存在栈上,当N大时,易产生栈溢出
    size_t _size;
}

注意:
1.浮点数、类对象以及字符串是不允许作为非类型模板参数的。 只 有 整 形 ( i n t s h o r t l o n g d o u b l e ) 、 指 针 、 引 用 被 支 持 。 \color{red}{只有整形(int short long double)、指针、引用被支持。} (intshortlongdouble)
2. 非类型的模板参数必须在编译期就能确认结果。 必 须 是 常 量 , 即 必 须 为 c o n s t 类 型 \color{red}{必须是常量,即必须为const类型} const
请试试下面的OJ题目,现在要求变了要求O(1)的时间复杂度完成。
求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)

采用静态函数+构造函数进行解题,复杂度O(1)

class Solution {
public:
    class Sum{
        public:
        Sum(){
            sum+=i;
            ++i;
        }
    };
    int Sum_Solution(int n) {
        sum=0;
        i=1;
        Sum arr[n];
        return sum;
    }
private:
    static int sum;
    static int i;
};
 
int Solution::sum=0;
int Solution::i=1;
 
/*class Solution {
public:
    int Sum_Solution(int n) {
        int sum=0;
        for(int i = 0;i<=n;i++){ 
            sum+=i;               // 牛客上的题检查的是真的很不严格
        }
        return sum;
    }
};*/

递归+逻辑运算符短路解题

class Solution {
public:
    int Sum_Solution(int n) {
        int a = 1;
        n > 1 && (a = n + Sum_Solution(n - 1));    // 逻辑运算符短路
        return a;
    }
};

黒知识:VS下递归编译阶段最多500次

2. 模板的特化

2.1 概念

通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结
果,
比如:

template<class T>
bool IsEqual(T& left, T& right)
{
    return left == right;
}
 
void Test()
{
    char* p1 = "hello";
    char* p2 = "world";
 	// 实际实际IsEqual比较char*类型地址
	// 字符串常量处于代码段
	// 先申请的地址小
    if(IsEqual(p1, p2))
        cout<<p1<<endl;
    else
        cout<<p2<<endl;
}

此时,就需要对模板进行特化即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化类模板特化

2.2 函数模板特化

函数模板的特化步骤:
1.必须要先有一个基础的函数模板 ,若特化对象不在,特化函数无法正常使用,要与重载分离开。
2. 关键字template后面接一对空的尖括号<>
3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。如const修饰错位问题,不如自己重新写。函数模板一般不建议特化。

template<>
bool IsEqual<char*>(char*& left, char*& right)
{
    if(strcmp(left, right) > 0)
        return true;
    
    return false;
}

注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。用户自定义函数与模板函数共存,且优先调用自定义函数。

bool IsEqual(char* left, char* right)
{
    if(strcmp(left, right) > 0)
        return true;
    
    return false;
}
2.3 类模板特化
2.2.1 全特化

全特化即是将模板参数类表中所有的参数都确定化

template<class T1, class T2> 
class Data
{
public:
    Data() {cout<<"Data<T1, T2>" <<endl;}
private:
    T1 _d1;
    T2 _d2;
};
 
template<> 
class Data<int, char>
{
public:
    Data() {cout<<"Data<int, char>" <<endl;}
private:
    T1 _d1;
    T2 _d2;
};
 
void TestVector()
{
	// 模板调用:只要有匹配的,如偏特化、全特化,均被优先调用。
	// 只要模板调用类型一致时,就会被调用。与函数内部使用该类型与否无关
    Data<int, int> d1;	// 类模板必须显示实例化。  其调用基本类模板
    Data<int, char> d2;  // 调用特化版本
} 
2.2.2 偏特化

偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。比如对于以下模板类:

template<class T1, class T2> 
class Data
{
public:
    Data() {cout<<"Data<T1, T2>" <<endl;}
private:
    T1 _d1;
    T2 _d2;
};

偏特化有以下两种表现方式

  • 部分特化
    将模板参数类表中的一部分参数特化
// 将第二个参数特化为int
template <class T1> 
class Data<T1, int>
{
public:
    Data() {cout<<"Data<T1, int>" <<endl;}
private:
    T1 _d1;
    int _d2;
}; 

//两个参数偏特化为指针类型 
template <typename T1, typename T2> 
class Data <T1*, T2*> 
{ 
public:
    Data() {cout<<"Data<T1*, T2*>" <<endl;}
    
private:
    T1 _d1;
    T2 _d2;
};
  • 参数更进一步的限制
    偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。
//两个参数偏特化为指针类型 
template <typename T1, typename T2> 
class Data <T1*, T2*> 
{ 
public:
    Data() {cout<<"Data<T1*, T2*>" <<endl;}
    
private:
    T1 _d1;
    T2 _d2;
};
 
//两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
    Data(const T1& d1, const T2& d2)
        : _d1(d1)
        , _d2(d2)
    {
            cout<<"Data<T1&, T2&>" <<endl;
    }
    
private:
    const T1 & _d1;
    const T2 & _d2;    
 };
 
void test2 () 
{
     Data<double , int> d1;      // 调用特化的int版本
     Data<int , double> d2;      // 调用基础的模板    
     Data<int *, int*> d3;       // 调用特化的指针版本
     Data<int&, int&> d4(1, 2);  // 调用特化的指针版本
}

指针、引用均可以进行特化,注意三类:引用(一个变量的别名,操作该别名变量与操作该对象一直,并且在构造函数初始化列表中也是别名,操作该别名与操作原变量一致)、const变量(c++11可以对const变量直接赋值)、无默认构造的对象必须以初始化列表进行初始化。

3. 类模板特化应用之类型萃取

问题:如何实现一个通用的拷贝函数?下面的实现有问题吗?
内置类型:浅拷贝、值拷贝、字节拷贝。
自定义类型:深拷贝

3.1 使用memcpy拷贝
template<class T>
void Copy(T* dst, const T* src, size_t size)
{
    memcpy(dst, src, sizeof(T)*size);
}
 
int main()
{
    // 试试下面的代码
    string strarr1[3] = {"11", "22", "33"};
    string strarr2[3];
    Copy(strarr2, strarr1, 3);
}

上述代码虽然对于任意类型的空间都可以进行拷贝,但是如果拷贝自定义类型对象就可能会出错,因为自定义类型对象有可能会涉及到深拷贝(比如string),而memcpy属于浅拷贝。如果对象中涉及到资源管理,就只能用赋值(自定义类型,有资源的申请释放)。

3.2 使用赋值方式拷贝
template<class T>
void Copy(T* dst, const T* src, size_t size)
{
    for(size_t i = 0; i < size; ++i)
    {
        dst[i] = src[i];
    }
}

循环赋值的方式虽然可以,但是代码的效率比较低,而C/C++程序最大的优势就是效率高。那能否将另种方式的优势结合起来呢?遇到内置类型就用memcpy来拷贝,遇到自定义类型就用循环赋值方式来做呢?

3.3 增加bool类型区分自定义与内置类型
template<class T>
void Copy(T* dst, const T* src, size_t size, bool IsPODType)
{
    if(IsPODType)
        memcpy(dst, src, sizeof(T)*size);
    else
    {
        for(size_t i = 0; i < size; ++i)
            dst[i] = src[i];
    }
}

通过多增加一个参数,就可将两种拷贝的优势体现结合起来。但缺陷是:用户需要根据所拷贝元素的类型去传递第三个参数,那出错的可能性就增加。那能否让函数自动去识别所拷贝类型是内置类型或者自定义类型呢?

3.4 使用函数区分内置与自定义类型

因为内置类型的个数是确定的,可以将所有内置类型集合在一起,如果能够将所拷贝对象的类型确定下来,在内置类型集合中查找其是否存在即可确定所拷贝类型是否为内置类型

// POD: plain old data 平凡类型(无关痛痒的类型)--基本类型
// 指在C++ 中与 C兼容的类型,可以按照 C 的方式处理。
//
// 此处只是举例,只列出个别类型
bool IsPODType(const char* strType)
{
    const char* arrType[] = {"char", "short", "int", "long", "long long", "float", 
"double", "long double"};
    for(size_t i = 0; i < sizeof(array)/sizeof(array[0]); ++i)
    {
        if(0 == strcmp(strType, arrType[i]))
            return true;
        }
    return false;
}
 
template<class T>
void Copy(T* dst, const T* src, size_t size)
{
    if(IsPODType(typeid(T).name()))
        memcpy(dst, src, sizeof(T)*size);
    else
    {
        for(size_t i = 0; i < size; ++i)
            dst[i] = src[i];
    }
}

通过typeid来确认所拷贝对象的实际类型,然后再在内置类型集合中枚举其是否出现过,既可确认所拷贝元素的类型为内置类型或者自定义类型。但缺陷是:枚举需要将所有类型遍历一遍,每次比较都是字符串的比较,效率比较低

3.5 类型萃取

为了将内置类型与自定义类型区分开,给出以下两个类分别代表内置类型与自定义类型。

// 代表内置类型
struct TrueType
{
    static bool Get()
    {
         return true ;
    }
};
// 代表自定义类型
struct FalseType
{
     static bool Get()
    {
         return false ;
    }
};

给出以下类模板,将来用户可以按照任意类型实例化该类模板。

template<class T>
struct TypeTraits
{
   typedef FalseType   IsPODType;
};

对上述的类模板进行以下方式的实例化:

template<>
struct TypeTraits<char>
{
   typedef TrueType     IsPODType;
};
 
template<>
struct TypeTraits<short>
{
   typedef TrueType     IsPODType;
};
 
template<>
struct TypeTraits<int>
{
   typedef TrueType     IsPODType;
};
 
template<>
struct TypeTraits<long>
{
   typedef TrueType     IsPODType;
};
// ... 所有内置类型都特化一下

通过对TypeTraits类模板重写改写方式四中的Copy函数模板,来确认所拷贝对象的实际类型。

/*
T为int:TypeTraits<int>已经特化过,程序运行时就会使用已经特化过的TypeTraits<int>, 该类中的
	   IsPODType刚好为类TrueType,而TrueType中Get函数返回true,内置类型使用memcpy方式拷贝
T为string:TypeTraits<string>没有特化过,程序运行时使用TypeTraits类模板, 该类模板中的IsPODType
刚好为类FalseType,而FalseType中Get函数返回true,自定义类型使用赋值方式拷贝
*/
template<class T>
void Copy(T* dst, const T* src, size_t size)
{
    if(TypeTraits<T>::IsPODType::Get())
        memcpy(dst, src, sizeof(T)*size);
    else
    {
    	 for(size_t i = 0; i < size; ++i)
            dst[i] = src[i];
    }
}
 
int main()
{
    int a1[] = {1,2,3,4,5,6,7,8,9,0};
    int a2[10];
    Copy(a2, a1, 10);
    
    string s1[] = {"1111", "2222", "3333", "4444"};
    string s2[4];
    Copy(s2, s1, 4);
    return 0;
}
3.6 STL中的类型萃取例子
// 代表内置类型
struct __true_type {};
 
// 代表自定义类型
struct __false_type {};
 
template <class type>
struct __type_traits 
{
typedef __false_type    is_POD_type;
};
 
// 对所有内置类型进行特化
template<>
struct __type_traits<char>
{
typedef __true_type    is_POD_type;
};
 
template<>
struct __type_traits<signed char>
{
typedef __true_type    is_POD_type;
};
 
template<>
struct __type_traits<unsigned char>
{
typedef __true_type    is_POD_type;
};
 
template<>
struct __type_traits<int>
{
typedef __true_type    is_POD_type;
};
 
template<>
struct __type_traits<float>
{
typedef __true_type    is_POD_type;
};
 
template<>
struct __type_traits<double>
{
typedef __true_type    is_POD_type;
};
 
// 注意:在重载内置类型时,所有的内置类型都必须重载出来,包括有符号和无符号,比如:对于int类型,必
须特化三个,int -- signed int -- unsigned int
 
// 在需要区分内置类型与自定义类型的位置,标准库通常都是通过__true_type与__false_type给出一对重载// 函数,然后用一个通用函数对其进行封装
// 注意:第三个参数可以不提供名字(匿名对象,但调用的时候必须写出),该参数最主要的作用就是让两个_copy函数形成重载
template<class T>
void _copy(T* dst, T* src, size_t n, __true_type)
{
memcpy(dst, src, n*sizeof(T));
}
 
template<class T>
void _copy(T* dst, T* src, size_t n, __false_type)
{
for (size_t i = 0; i < n; ++i)
dst[i] = src[i];
}
 
template<class T>
void Copy(T* dst, T* src, size_t n)
{
_copy(dst, src, n, __type_traits<T>::is_POD_type());	// 匿名对象,传类型相应对象作为实参传入,必须加()
}

4 模板分离编译

4.1 什么是分离编译

一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。(内联函数inline不建议分离编译,在项目中不在同一个文件中时及写成两个文件时的情况)

4.2 模板的分离编译

假如有以下场景,模板的声明与定义分离开,在头文件中进行声明,源文件中完成定义:

// a.h
template<class T>
T Add(const T& left, const T& right);
 
// a.cpp
template<class T>
T Add(const T& left, const T& right)
{
    return left + right;
}
 
// main.cpp
#include"a.h"
int main()
{
    Add(1, 2);
    Add(1.0, 2.0);
    
    return 0;
}
4.3 解决方法

1.将声明和定义放到一个文件 “xxx.hpp” 里面或者xxx.h其实也是可以的。推荐使用这种
2.模板定义的位置显式实例化。这种方法不实用,不推荐使用
分离编译扩展阅读
在这里插入图片描述
模板的实现与它的.h放到一个文件即可。

5. 模板总结

【优点】
1.模板复用了代码,节省资源更快的迭代开发,C++的标准模板库(STL)因此而产生
2. 增强了代码的灵活性
3. 模板在进行完封装后使用起来比较爽,STL的以模板为主体实现,vector多爽不多解释了
4. 可维护性高

3、4点为重要核心点

【缺陷】
1.模板会导致代码膨胀问题,也会导致编译时间变长(华为:过度 .h的过度嵌套,浪费编译时间)
2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ypuyu

如果帮助到你,可以请作者喝水~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值