【C++】模板进阶

简介: 【C++】模板进阶

钱塘江上潮信起,今日方知我是我!d2cc73ddd16441838c851cecd1b1c131.jpeg



一、非类型模板参数(用整型类型变量来作为模板的参数,传参时只能传常量)

1.非类型模板参数的规定


1.

模板参数分为类型模板参数和非类型模板参数,类型模板参数一般是class或typename定义出来的泛型,而非类型模板参数一般是整型定义出来的常量,这个常量作为类模板或函数模板的一个参数,在类模板或函数模板中可将该参数当成常量来使用。


2.

在C语言阶段如果想要让数组的大小可以自己控制,一般都会用定义宏的方式来解决,在C++中我们可以使用非类型模板参数来进行解决,下面代码给出类模板的声明,在使用时我们可以显示实例化类模板,给非类型模板参数传一个常量,这个常量在类中可以任意使用。

//#define N 10
//静态数组
//非类型模板参数 --- 是一个常量
template<class T, int N = 10>//给非类型模板参数缺省值为常量10
class Array
{
private:
  T _a[N];
};
int main()
{
  //Array<int> a1;    // 10
  //Array<double> a2;   // 1000
  Array<int,10> a1;   // 10
  Array<double,1000> a2;  // 1000
  return 0;
}



3.

非类型模板参数声明时的类型必须只能是整型,其他例如自定义类型,字符串类型,浮点型等类型均不能作为非类型模板参数的类型声明,只有整型才可以。


4.

在显式实例化模板时,给非类型模板参数传参时,只能给常量,不能给变量,否则会报错:局部变量不能作为非类型模板参数。所以在传参时,也只能传常量。

template<class T,size_t N>
//template<class T,double N>
//浮点数不能作为非类型模板参数,非类型模板参数基本都是整型,int short char,像自定义类型,字符串,浮点型这些都不可以。
void Func(const T& x)
{
  cout << N << x << endl;
}
int main()
{
  //Func<int, 100>(1);
  //Func<int, 100.11>(1);
  //int a = 10;
  //Func<int, a>(1);//只能给常量,局部变量不能作为非类型模板参数。
  //非类型模板参数主要还是用于,在类里面定义某些数组时,想要通过非类型模板参数的大小来定义数组大小。
  return 0;
}



2.C++的array类对标C语言静态数组


1.

C++搞出来非类型模板参数的array类,实际对标的就是C语言的静态数组,array的第二个模板参数就是非类型模板参数N,我们在定义静态数组时,除C语言外的定义方式,还可以用array类来定义一个对象,这个对象的底层实际就是静态数组,他们都存在栈上。


126eb93a58d64325a157cf620584834c.png


2.

两者的区别就是对于越界访问的检查机制不同,C语言对于越界访问读不检查,对于临近位置的越界访问写,一般会检查出来并报错,但对于较远位置的越界访问写,就不一定能够检查出来了,因为C语言对于越界访问写采用的是一种抽查的机制。

C++觉得C语言的检查机制不够严格,使用者在使用时有可能会因为越界访问导致程序出现意料不到的错误,所以C++出来了array类,array无论对于越界读还是越界写,他都可以检查出来,本质是因为他的检查机制是assert,如果访问的下标超过array的size,则直接会断言报错,不存在检查不到的情况。

我们在模拟实现vector时,采用的也是库检查机制相同的代码,直接进行assert断言检查。

dee02244fdc64f759ee160a03528596a.png


int main()
{
  //C++11想让我们用array,不想让我们用C语言的数组了,就因为C语言的越界检查不够严格,容易引发问题。
  int a1[10];
  array<int, 10> a2;//array对比的是C语言的静态数组,并且两个数组都在栈上
  array<int, 100> a3;
  //两者的区别,对于越界的检查: 
  //C语言对于越界读,不检查。越界写,是抽查。
  cout << a1[10] << endl;
  cout << a1[15] << endl;
  //a1[10] = 0;//临近位置进行抽查,一般能够抽查出来。
  a1[15] = 0;//但是位置如果离的远了,不一定能抽查出来。
  //C++11的array无论是越界读,还是越界写,都可以检查出来。
  //因为我们模拟实现operator[]时,采用的就是assert断言报错,库的实现也是这样的。
  cout << a2[10] << endl;
  cout << a2[15] << endl;
  //a2[10] = 0;//临近位置进行抽查,一般能够抽查出来。
  a2[15] = 0;
  return 0;
}


二、模板的特化(模板的特殊实例化)

1.函数模板的特化(建议使用重载函数,不建议进行特化)


1.

在main函数的测试用例中前两次的打印结果都是正常的,因为日期之间进行比较时可以直接调用日期类的运算符重载,并且Less是一个函数模板,可以接收所有的类型的比较,包括内置类型和自定义类型。


2.

但是当Less模板类型为日期类指针类型时,打印的结果就会有问题了,因为比较的是两个日期对象的地址,而地址是随机的,这时候对于日期类指针这种类型,函数模板Less就会出问题。


3.

既然是针对日期类指针类型出现的问题,那就可以通过函数模板的特化来解决,我们将Date*这样的类型单独拿出来实例化出一个现成的函数来,这样的方式就被称作函数模板的特化。


4.

在函数模板特化时,必须要先有一个基础的函数模板,然后在特化的函数上一行加一个template<>,然后在函数名后面加尖括号,尖括号里面指定特化的类型,特化函数的形参表必须要和原来的函数模板的形参表所包含的基础参数类型匹配,若不匹配,则编译器会报一些奇怪的错误。


5.

一般情况下,在遇到函数模板不能解决或者处理有误的类型时,为了实现简单,通常是用重载函数来解决的,这样的代码可读性高,容易书写。而如果遇到参数类型十分复杂的模板时,特化时需要特别给出,书写起来较为繁琐,不如直接重载函数来的快。

bool Less(Date* left, Date* right)
{
  return *left < *right;
}


class Date
{
public:
  Date(int year = 1900, int month = 1, int day = 1)
    : _year(year)
    , _month(month)
    , _day(day)
  {}
  bool operator<(const Date& d)const
  {
    return (_year < d._year) ||
      (_year == d._year && _month < d._month) ||
      (_year == d._year && _month == d._month && _day < d._day);
  }
  bool operator>(const Date& d)const
  {
    return (_year > d._year) ||
      (_year == d._year && _month > d._month) ||
      (_year == d._year && _month == d._month && _day > d._day);
  }
  friend ostream& operator<<(ostream& _cout, const Date& d)
  {
    _cout << d._year << "-" << d._month << "-" << d._day;
    return _cout;
  }
private:
  int _year;
  int _month;
  int _day;
};
// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
  return left < right;
}
// 针对某些类型进行特殊处理 -- Date*
template<>
bool Less<Date*>(Date* left, Date* right)
{
  return *left < *right;
}
// 下面这种方式是函数重载
bool Less(Date* left, Date* right)
{
  return *left < *right;
}
int main()
{
  cout << Less(1, 2) << endl;   // 可以比较,结果正确
  Date d1(2022, 7, 7);
  Date d2(2022, 7, 8);
  cout << Less(d1, d2) << endl;  // 可以比较,结果正确
  Date* p1 = &d1;
  Date* p2 = &d2;
  cout << Less(p1, p2) << endl;  // 可以比较,结果错误,地址之间的逻辑比较,可能为0可能为1,因为地址是随机的。
  return 0;
}


2.类模板的全特化(类模板实例化出具体的类)


1.

类模板的全特化就是将模板参数列表中所有的参数都确定化,在显示实例化函数模板时,若显示所传参数均为double,则不会走推演实例化的步骤,而是直接走实例化好的类,所以类模板的全特化实际就是在参数确定之后,模板实例化出具体的类。

template<class T1, class T2>
class Data
{
public:
  Data() { cout << "Data<T1, T2>" << endl; }
private:
  T1 _d1;
  T2 _d2;
};
// 类模板的特化 --- 全特化
template<>
class Data<double,double>
{
public:
  Data() { cout << "Data<double, double>" << endl; }
private:
};


3.类模板的偏特化

3.1 部分特化


1.

部分特化后的模板属于办成品,如果在传参时,某一个参数是属于部分特化后的参数,则编译器优先调用那个部分特化的类模板。

// 类模板的特化 --- 半特化、偏特化
template<class T1>
class Data<T1, char>
{
public:
  Data() { cout << "Data<T1, char>" << endl; }
private:
};
int main()
{
  Data<int, char> d3;// d3和d4都匹配半特化后的模板
  Data<double, char> d4;
}


3.2 对参数的进一步限制


1.

除部分特化外,类模板的偏特化还可以对参数进行借一步的限制,如下两个模板,分别针对指针和引用这样的形式进行限制,只要所传参数均为引用或指针时,编译器优先调用下面这两个偏特化后的类模板。

// 对参数类型的进一步限制 --- 半特化
template<class T1, class T2>
class Data<T1*, T2*>//单独对指针类型进行特化,无论是什么类型的指针。
{
public:
  Data() { cout << "Data<T1*, T2*>" << endl; }
};
template<class T1, class T2>
class Data<T1&, T2&>//单独对引用进行特化
{
public:
  Data() { cout << "Data<T1&, T2&>" << endl; }
};


4.模板特化的本质(编译器对于参数的匹配原则:优先匹配现成的模板)


1.

无论是类模板的全特化还是偏特化的部分特化或对参数的进一步限制,其本质还是编译器对于模板参数的优先匹配原则。

只要有现成实例化好的模板,编译器肯定不会去费力推导实例化模板。


2.

下面的main函数用于验证编译器对于模板的优先匹配机制,编译器总是会优先调用现成的模板,实在没有的时候,编译器才会自己推演实例化。

int main()
{
  Data<int, int> d1;
  Data<double, double> d2;
  // 优先级队列对于日期类当时的处理方式是显示写仿函数。如果不用仿函数就用原来的类来进行比较,可以通过仿函数类特化来解决
  //将仿函数这个类进行特化,让仿函数对T为Date*类型时进行特殊处理,改为解引用后的内容之间进行比较即可。不用重新写仿函数。
  // 实际特化时,针对的都是较小的类来进行特化的,比如仿函数这样较小的类。
  Data<int, char> d3;// d3和d4都匹配半特化后的模板
  Data<double, char> d4;
  // 有成品吃成品,没有成品吃半成品,没有半成品就用原材料自己做,全特化、半特化、未特化模板
  Data<int*, int*> d5;
  Data<char*, int*> d6;
  Data<char, int*> d7;
  Data<double&, int&> d8;
  //特化的本质体现的是编译器参数的匹配原则,有现成实例化出来的就优先用现成的,没有现成用半成,没有办成就自己进行实例化。
  //遇到一堆模板报出来的错误的时候,先去看第一个错误,不要慌,第一个解决了下面基本就都解决了
  //有的时候模板错误的定位不准,
  return 0;
}


三、类模板分离编译(类模板不要声明和定义分离)


1.

在使用类模板显示实例化的地方,只有.h文件展开,而没有.cpp文件,因为在链接之前,各源文件之间是互不联系的,所以即使你显示实例化了类模板,但在类模板真正定义的地方却没有实例化,所以在链接的时候.cpp里面没有实例化出来的类模板,自然链接就会出问题,因为你用了一个并没有真正实例化出来的类,编译器就会报链接错误。


2.

解决的方式也很简单,有两种方法,将声明和定义放到一个文件 “xxx.hpp” 里面或者xxx.h文件里面,但一般喜欢用.hpp文件,这代表这个文件专门用来放类模板的声明和定义。

第二种就是在模板定义的位置也就是.cpp文件里面进行对应模板参数类型的显式实例化,但这种方式不推荐,如果我要实例化出10个类呢?那你就在类模板定义的地方连续显示实例化出10个类吗?这样的方式未免有些太挫了吧!


类模板能否声明和定义分离?


分离编译扩展阅读:为什么C++编译器不能支持对模板的分离式编译?



四、模板总结


1.

模板最大的优点就是可以进行泛型编程,并且能够进行代码的复用,提升了代码的可维护性,这也为STL(标准模板库)的产生奠定了基础。

能够泛型编程并且退出STL库才是C++真正拉开与C语言之间的距离的标志。


2.

但代码复用也会带来缺点,模板在实例化时,如果实例化出多个类,则会导致代码膨胀,增加编译器编译的时间。

由于模板的泛型性质,在报模板错误时,错误信息会非常的凌乱,不容易定位错误的具体位置,但大家在遇到模板大量的报错信息时,不要慌张,先去看报错的第一条信息,解决第一条错误信息后,后面的许多错误可能也会被解决掉了。


3.虽然模板也有错误,但总体来说,模板的优点一定是大于其缺点的。































































相关文章
|
2月前
|
存储 算法 C++
C++ STL 初探:打开标准模板库的大门
C++ STL 初探:打开标准模板库的大门
112 10
|
4月前
|
编译器 C++
【C++】——初识模板
【C++】——初识模板
【C++】——初识模板
|
23天前
|
安全 编译器 C++
【C++11】可变模板参数详解
本文详细介绍了C++11引入的可变模板参数,这是一种允许模板接受任意数量和类型参数的强大工具。文章从基本概念入手,讲解了可变模板参数的语法、参数包的展开方法,以及如何结合递归调用、折叠表达式等技术实现高效编程。通过具体示例,如打印任意数量参数、类型安全的`printf`替代方案等,展示了其在实际开发中的应用。最后,文章讨论了性能优化策略和常见问题,帮助读者更好地理解和使用这一高级C++特性。
38 4
|
23天前
|
算法 编译器 C++
【C++】模板详细讲解(含反向迭代器)
C++模板是泛型编程的核心,允许编写与类型无关的代码,提高代码复用性和灵活性。模板分为函数模板和类模板,支持隐式和显式实例化,以及特化(全特化和偏特化)。C++标准库广泛使用模板,如容器、迭代器、算法和函数对象等,以支持高效、灵活的编程。反向迭代器通过对正向迭代器的封装,实现了逆序遍历的功能。
33 3
|
5月前
|
程序员 C++
C++模板元编程入门
【7月更文挑战第9天】C++模板元编程是一项强大而复杂的技术,它允许程序员在编译时进行复杂的计算和操作,从而提高了程序的性能和灵活性。然而,模板元编程的复杂性和抽象性也使其难以掌握和应用。通过本文的介绍,希望能够帮助你初步了解C++模板元编程的基本概念和技术要点,为进一步深入学习和应用打下坚实的基础。在实际开发中,合理运用模板元编程技术,可以极大地提升程序的性能和可维护性。
|
26天前
|
编译器 C++
【c++】模板详解(1)
本文介绍了C++中的模板概念,包括函数模板和类模板,强调了模板作为泛型编程基础的重要性。函数模板允许创建类型无关的函数,类模板则能根据不同的类型生成不同的类。文章通过具体示例详细解释了模板的定义、实例化及匹配原则,帮助读者理解模板机制,为学习STL打下基础。
29 0
|
2月前
|
编译器 程序员 C++
【C++打怪之路Lv7】-- 模板初阶
【C++打怪之路Lv7】-- 模板初阶
18 1
|
2月前
|
编译器 C语言 C++
C++入门6——模板(泛型编程、函数模板、类模板)
C++入门6——模板(泛型编程、函数模板、类模板)
54 0
C++入门6——模板(泛型编程、函数模板、类模板)
|
2月前
|
算法 编译器 C++
【C++篇】领略模板编程的进阶之美:参数巧思与编译的智慧
【C++篇】领略模板编程的进阶之美:参数巧思与编译的智慧
84 2
|
2月前
|
存储 编译器 C++
【C++篇】引领C++模板初体验:泛型编程的力量与妙用
【C++篇】引领C++模板初体验:泛型编程的力量与妙用
42 2