【C++篇】模板进阶:依赖类型、特化等高级技巧

【C++篇】模板进阶:依赖类型、特化等高级技巧

💬 欢迎讨论:在阅读过程中有任何疑问,欢迎在评论区留言,我们一起交流学习! 👍 点赞、收藏与分享:如果你觉得这篇文章对你有帮助,记得点赞、收藏,并分享给更多对C++感兴趣的朋友

文章目录

前言一、typename解决依赖类型问题1)什么是依赖类型(Dependent Type)?2)为什么依赖类型需要 typename?

二、非类型模板参数三、模板的特化1)什么是特化?2)函数模板特化3)类模板的特化3.1 全特化3.2 偏特化(半特化)

四、模板的分离编译

前言

本文将补充一些模板的知识,现在我们对模板的基本使用已经了解(【C++篇】C++模板初阶:从泛型编程到函数模板与类模板的全面解析)。然而,模板还有些高级用法,可以解决一些特殊问题,尽管用的不多,但必不可少。

【本文目标】

了解什么是依赖类型使用typename解决模板依赖类型问题了解非类型模板参数掌握函数特化和类特化了解模板的分离编译

一、typename解决依赖类型问题

在我们定义模板类型的时候,class与typename是没有任何区别的:

template // 等价于

template

但在下面这个情况下,我们就需要注意了:

例:一个通用的打印函数

#include

#include

#include

template

void Print(const Container& v)

{

Container::const_iterator it = v.begin();

//typename Container::const_iterator it = v.begin();正确写法

while (it != v.end())

{

cout << *it << " ";

++it;

}

cout << endl;

}

int main()

{

vector v;

v.push_back(1);

v.push_back(2);

v.push_back(3);

v.push_back(4);

Print(v);

list lt;

lt.push_back(1);

lt.push_back(2);

lt.push_back(3);

lt.push_back(4);

Print(lt);

return 0;

}

运行起来可以发现,编译都不通过! 为什么呢? 原来: 编译器无法确定这里的Container是什么(未被实例化),导致编译器无法确定Container::const_iterator是类型还是静态成员变量等,造成编译出错。

解决办法: 在Container::const_iterator前面添加一个typename,告诉编译器,这是一个类型。 或者可以用auto(auto就是类型),让编译器根据等号后面的确定变量来推导这个类型。

//typename Container::const_iterator it = v.begin();

auto it = v.begin();

这里的Container::const_iterator其实就是一个依赖类型。

1)什么是依赖类型(Dependent Type)?

在C++模板中,依赖类型(Dependent Type)是指依赖于模板参数的类型。

依赖类型的特性:

依赖于模板参数

依赖类型通常是通过模板参数(如 T)的嵌套访问得到的,例如 T::SubType 或 T::iterator。 编译时无法确定

在模板定义阶段,编译器不知道 T 的具体类型,因此无法判断 T::SubType 是类型、静态成员变量还是其他内容。 需要显式声明

必须用 typename 显式告知编译器 T::SubType 是一个类型,否则编译器会默认假设它是非类型(如静态成员变量或函数)。

依赖类型的常见场景:

嵌套类型

template

void foo() {

typename T::SubType x; // T::SubType 是依赖类型,必须加 typename

}

模板中的迭代器类型

上文所展示的案例就是一个经典的迭代器类型

template

void printFirst(const Container& c) {

typename Container::const_iterator it = c.begin(); // 依赖类型

std::cout << *it << std::endl;

}

Container::const_iterator 是依赖类型,因为 Container 是模板参数。

2)为什么依赖类型需要 typename?

编译器解析的歧义:编译器无法确定这是类型还是静态成员

template

void func() {

T::SubType x; // 编译器无法确定这是类型还是静态成员

}

如果 T 是 MyClass,且 MyClass 有一个静态成员 SubType,则 T::SubType x 是合法的变量声明。如果 T 是otherclass,且 otherClass 有一个嵌套类型 SubType,则 T::SubType x 是合法的类型声明。

也许你会说:类是自己定义的,我难道不能控制避免让静态变量和类型重名吗? 遗憾的是,这些类并没有被实例化,编译器是不知道你在类中干了什么的。

typename 作用就是: 通过 typename 显式告诉编译器:T::SubType 是一个类型,而非其他内容。

二、非类型模板参数

C++模板中除了类型模板之外,还有非类型模板参数,它们允许在编译时将具体的值传递给模板。

template // T是类型参数,N是非类型模板参数

class Array {

// ...

};

非类型模板参数有两个条件:

常量整型

也就是说,非类型模板参数是不可修改的(常量),至于为什么只能是整型,可能是因为浮点型作为非类型模板参数几乎无意义吧(我猜的😁当个乐子听就好)

【使用示例】

template

class FixedArray {

private:

T data[size];

public:

int getSize() const { return size; }

// ...

};

// 使用

FixedArray arr; // 创建一个大小为10的int数组

三、模板的特化

1)什么是特化?

顾名思义,他可以为特定的模板参数类型提供专门的实现,从而满足特定需求或处理某些类型的特殊情况。 即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。 模板的特化分为两种:

函数模板特化类模板的特化

2)函数模板特化

函数模板的特化步骤:

必须要先有一个基础的函数模板关键字template后面接一对空的尖括号<>函数名后跟一对尖括号,尖括号中指定需要特化的类型函数形参表:必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误

栗子:

// 函数模板 -- 参数匹配

template

bool Less(T left, T right)

{

return left < right;

}

// 对Less函数模板进行特化

template<>

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; // 调用特化之后的版本,而不走模板生成了

return 0;

}

🎗️:一般情况下,遇到需要特殊处理的函数模板参数,为了实现简单,通常都是将该函数直接实现,不用模板。因此,函数模板一般是不建议特化的。

3)类模板的特化

类模板特化分为全特化和偏特化两种形式。 特化步骤与函数模板特化是类似的:

必须要先有一个基础的类模板关键字template后面接一对空的尖括号<>类名后跟一对尖括号,尖括号中指定需要特化的类型

3.1 全特化

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

template

class Data

{

public:

Data() {cout<<"Data" <

private:

T1 _d1;

T2 _d2;

};

template<>

class Data

{

public:

Data() {cout<<"Data" <

private:

int _d1;

char _d2;

};

void TestVector()

{

Data d1;

Data d2;

}

输出结果

Data Data\

特化成功。

3.2 偏特化(半特化)

偏特化是任何针对模版参数进一步进行条件限制设计的特化版本。 下文将以这个类进行偏特化:

template

class Data

{

public:

Data() {cout<<"Data" <

private:

T1 _d1;

T2 _d2;

};

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

部分特化 将模板参数类表中的一部分参数特化。

// 将第二个参数特化为int

template

class Data

{

public:

Data() {cout<<"Data" <

private:

T1 _d1;

int _d2;

};

参数更进一步的限制 偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本

//两个参数偏特化为指针类型

template

class Data

{

public:

Data() {cout<<"Data" <

private:

T1 _d1;

T2 _d2;

};

//两个参数偏特化为引用类型

template

class Data

{

public:

Data(const T1& d1, const T2& d2)

: _d1(d1)

, _d2(d2)

{

cout<<"Data" <

}

private:

const T1 & _d1;

const T2 & _d2;

};

四、模板的分离编译

分离编译就是一个程序由若干个源文件共同实现. 我们平时用的最多的就是用.h头文件 + .c方法文件 + test.c测试文件来完成一个程序。这种实现方式就是分离编译。 但自从接触模板以后,我们似乎没有再去使用这种方法去实现程序了,我们的函数实现是直接在头文件中实现的。

那为什么要抛弃分离编译呢?

我们可以去试试模板的分离编译

//.h文件

template>

class stack

{

public:

void push(const T& x);

void pop();

T& top()

{

return _con.back();

}

size_t size()

{

return _con.size();

}

bool empty()

{

return _con.empty();

}

private:

Container _con;

};

//.c文件

template

void stack::push(const T& x)

{

_con.push_back(x);

}

template

void stack::pop()

{

_con.pop_back();

}

运行起来可以发现,编译器报错了,编译器找不到我们实现的方法(即使头文件我们声明了)

乍一看,原来模板没有被实例化

这个好办,我们对每个实现方法显示实例化一下,运行起来,成了!

template

class stack;

template

class stack;

但是我们想一想,如果我们有上百个实现方法呢?难道我们要去显示实例化个上百次吗?这样肯定是不行的!

解决方法:

将声明和定义放到一个文件 “.hpp” 里面。在.h里直接实现(推荐)模板定义的位置显式实例化。(这种方法不实用,极不推荐)

完~ 下篇预告:继承

黄金推荐

伊苏8如何快速积攒素材 伊苏8素材获得方法
365体育投注注册

伊苏8如何快速积攒素材 伊苏8素材获得方法

✨ 07-16 💎 价值: 7514
瑜伽種類有哪些?從入門到高階,找到適合您的瑜伽課程
狙击手幽灵战士3怎么手动存档 游戏手动存档位置及方法介绍
英国365网址是多少

狙击手幽灵战士3怎么手动存档 游戏手动存档位置及方法介绍

✨ 07-25 💎 价值: 7967