当前位置: 首页 > article >正文

【C++】模板使用总结

文章目录

    • 模板函数
      • 模板参数的声明
      • 模板函数的定义和实例化
      • 模板函数重载
    • 类模板
      • 类模板的特化
      • 局部特化
      • 缺省模板实参
      • 注意
    • 非类型模板参数
      • 非类型的类模板参数
      • 非类型的函数模板参数
      • 非类型模板参数的限制
    • 技巧性基础知识
      • 关键字typename
      • 使用this->
      • 成员模板
      • 模板作为模板参数
      • 零初始化
      • 使用字符串作用函数模板的实参
    • 包含模式
    • 静多态
      • 动多态和静多态两者的优点
      • 使用模板实现Bridge Pattern

模板函数

模板参数的声明

template<comma-separated-list-of-parameters> // template<用逗号隔开的参数列表>

模板函数的定义和实例化

模板函数的定义:

template<typename T>
inline T const& max(T const& a, T const& b)
{
    return a < b ? b : a;
}

**模板函数的实例化:**使用具体类型代替模板参数。只要使用函数模板,编译器会自动进行实例化。

int a = max(1, 2);            // 使用int实例化
float b = max(4.0f, 3.0f);    // 使用float实例化

// 实例化时,编译器会自动生成实例化的代码。比如使用int实例化会生成如下代码:
inline int const& max(int const& a, int const& b)
{
    return a < b ? b : a; // int支持a < b的调用
}

注意:用于实例化模板的类型,必须支持模板函数的所有操作

class A{}
A a1, a2;
A a3 = max(a1, a2); // 编译报错:A不支持<运算符

模板的编译:

  1. 实例化之前,先检查模板代码本身,查看语法是否正确;
  2. 实例化期间,检查模板代码,查看实参的所有调用是否都有效。

实参的推导:

模板可以根据实参的类型来推导出实例化的类型,但所有的实参必须正确匹配类型,不支持自动转换类型。

max(4, 7); // OK:两个实参的类型都是int,使用int实例化
max(4, 7.0); // ERROR: 两个实参的类型不同,不支持自动转换类型

// 解决方法
max(static_cast<double>(4), 7.0);    // 强制类型转换
max<double>(4, 4.2);                 // 显示指定T的类型

模板函数重载

可以使用模板函数重载非模板函数

inline int const& max(int const& a, int const& b)
{
    return a < b ? b : a;
}

// 使用模板函数重载
template<typename T>
inline T const& max(T const& a, T const& b)
{
    return a < b ? b : a;
}

int main()
{
    max(2, 3);		   // 当模板和非模板函数都匹配时,优先匹配非模板函数
    max(2.0f, 3.0f);   // 模板函数有更优的匹配,调用模板函数
    max<>(2, 3);	   // 显示声明调用模板函数
}

类模板

// xx.h文件
#include <stdexcept>
#include <vector>

template<typename T>
class Stack
{
private:
    std::vector<T> elems;
public:
    void push(T const&);
    void pop();
    T top() const;
    bool empty() const
    {
         return elems.empty();   
    }
}

template<typename T>
void Stack<T>::push(T const& elem)
{
    elems.push_back(elem);
}

template<typename T>
void Stack<T>::pop()
{
    if(elems.empty()){
        throw std::out_of_range("Stack<>::pop():empty stack");
    }
    elems.pop_back();
}

template<typename T>
T Stack<T>::top() const
{
    if(elems.empty()){
        throw std::out_of_range("Stack<>::top():empty stack");
    }
    return elems.back();
}

//===================================================================
// main.cpp文件
#include <iostream>
#include "xx.h"

int main()
{
	try{
        Stack<int> intStack;
        Stack<std::string> stringStack; 
        
        intStack.push(7);
        std::cout << intStack.top() << std::endl;
        
        stringStack.push("hello");
        std::cout << stringStack.top() << std::endl;
        stringStack.pop();
        stringStack.pop();
    }
    catch(std::exception const& ex){
        std::cerr << "Exception:" << ex.what() << std::endl;
    }
}

注意:基于不同类型的实例化,会生成不同的类型,比如:Stack<int>和Stack<float>是不同的类型。

类模板的特化

可以用实参来特化类模板,和函数模板的重载类似,可以指定特定类型的实现,来克服实例化模板的不足。

比如,实例化的实参必须支持模板参数的所有调用。对于不支持的类型,可以重载实现:

struct Number
{
    int value;
}

template<typename T>
inline T const& max(T const& a, T const& b)
{
    return a < b ? b : a;
}

// 使用Number类型对模板函数进行特化,Number类型不支持max()的模板参数的'<'的调用,
// 特化对其进行特殊处理。
inline Number const& max(Number const& a, Number const& b)
{
    return a.value < b.value ? b : a;
}

类模板的特化也是相同的道理,相当于对类重新实现。

// 对上面的Stack类模板进行特化,模板参数T替换成string
#include <stdexcept>
#include <deque>
using namespace std;

template<>    // 在class前面用template<>来表示特化
class Stack<string>
{
private:
    // 使用deque来管理内部元素,而不是vector
    std::deque<string> elems;	
public:
    void push(string const&);
    void pop();
    string top() const;
    bool empty() const
    {
         return elems.empty();   
    }
    
    // 添加full()成员函数,特化相当于是重新实现stack类,不需要和模板保存相同
    bool full() const{  
        return elems.full();
    }
}

// 特化后成员函数不是模板,不用加template
void Stack<string>::push(string const& elem) 
{
    // 这里和模板的实现相同,但特化的实现可以模板的实现完全不同
    elems.push_back(elem);
}

...略

局部特化

当有多个模板参数时,可以对部分模板参数进行局部特化

template<typename T1, typename T2>
class MyClass
{
    ...
}

// 两个模板参数相同
template<typename T>
class MyClass<T, T>
{
    ...
}

// 对第二个模板参数进行局部特化
template<typename T>
class MyClass<T, int>
{
    ...
}

// 对两个模板参数进行特化
template<typename T1, typename T2>
class MyClass<T1*, T2*>
{
    ...
}

// 局部特化可能产生二义性,某个实例化可以匹配多个局部特化时就会产生二义性而报错
Myclass<int, int> m;	// 可以匹配MyClass<T, T>和MyClass<T, int>,报错

缺省模板实参

可以为类模板的模板参数定义缺省值,这个缺省值称为缺省模板实参,并可以引用它前面的模板参数。

// xx.h
template<typename T, typename CONT = std::vector<T>> // std::vector<T>的T引用了前面的T
class Stack
{
private:
    // 内部容器的类型可以使用模板参数指定
	CONT elems;
public:
	void push(T const&);
	void pop();
	T top() const;
	bool empty() const{
		return elems.empty();
	}
}

template<typename T, typename CONT>
void Stack<T, CONT>::push(T const& elem)
{
    elems.push_back(elem);
}
...略

// Stack模板的使用
Stack<int> intStack;                         // CONT = 缺省值(std::vector<int>)
Stack<double, std::deque<double>> dblStack;  // CONT = std::deque<double>

注意

类模板的实例化有个特殊的地方是:只有被调用的成员函数才会被实例化。对于开头的Stack<T>类模板,比如:

Stack<int> intStack;
intStack.push(1);

// 如果整个程序中Stack<int>只调用了push()函数,那么它的实例化代码如下:
class Stack<int>
{
private:
    std::vector<int> elems;
public:
    // 只有push()被调用,因此只有push()被实例化
    void push(int const&);
}

void Stack<int>::push(int const& elem)
{
    elems.push_back(elem);
}

非类型模板参数

非类型的类模板参数

template<typename T, int MAX_SIZE>  // MAX_SIZE不是类型,也可以作为模板参数
class Stack
{
private:
	T elems[MAX_SIZE];
    int numElems;
public:
    Stack();
    void push(T const&);
    void pop();
    T top() const;
    bool empty() const{
        return numElems == 0;
    }
    bool full() const{
        return numElems == MAX_SIZE;
    }
}

template<typename T, int MAX_SIZE>
Stack<T, MAX_SIZE>::Stack(): numElems(0)
{
    
}

template<typename T, int MAX_SIZE>
void Stack<T, MAX_SIZE>::push(T const& elem)
{
    elems[numElems] = elem;
    numElems++;
}
....略

// 使用
Stack<int, 20> int20Stack;
Stack<int, 40> int40Stack;

// 也可以使用缺省值
template<T, int MAX_SIZE = 20>
class Stack
{
    
}

Stack<int> intStack;  // Stack<int, 20>

注意:非类型的模板参数不同也是不同的类型,Stack<int, 20>和Stack<int, 40>是不同的类型

非类型的函数模板参数

template<typename T, int VAL>
T addValue(T const& x)
{
    return x + VAL;
}

// 也可以使用缺省值
template<typename T, int Val = 5>
T addValue(T const& x)
{
    return x + VAL;
}

非类型模板参数的限制

合法:常整数(包括枚举值),指向外部链接对象的指针

非法:浮点数(历史遗留),类对象,字符串,指向内部链接对象的指针

template<char const* name>
class MyClass
{
    ...
}

char const* s = "hello"; 
MyClass<s> x;	// ERROR:s为内部链接对象

extern char const* s = "hello"; 
MyClass<s> x;	// OK:s为外部链接对象

(不确定,根据相关只是猜测) 如果是内部对象,则只被翻译单元所拥有,而编译发生在编译单元内,不同的编译单元内会生成相同的类型,而链接的时候,因为是内部链接而无法识别,导致不能优化重复类型。而使用外部链接对象编译时,链接的时候可以识别为相同的类型而进行优化。

技巧性基础知识

关键字typename

如果要访问依赖模板参数的类型名称,要在类型名称前添加关键字typename

class Type
{
    class SubType
    {
        
    }
}

template<typename T>
class MyClass
{
    typename T::SubType* ptr;  // typename声明SubType是定义于类T内部的一种类型
    T::SubType* ptr; // 不使用typename,T::SubType表示T中的静态成员,表示乘法操作
    ...
}

使用this->

如果基类是类模版,不能直接调用基类的成员,必须用this->去调用

template<typename T>
class Base
{
public:
	void exit1()
	{
		std::cout << "call Base::exit1()." << std::endl;
	}
};

void exit1()
{
	std::cout << "call exit1()." << std::endl;
}

template<typename T>
class Derived : public Base<T>
{
public:
	void foo()
	{
		exit1();			// call exit1().
		this->exit1();      // call Base::exit1().
        Base<T>::exit1();	// call Base::exit1().
	}
};

成员模板

嵌套类和成员函数也可以是模板

template<typename T>
class Stack
{
private:
    std::deque<T> elemes;
public:
    // 成员函数模板
	template<typename T2>
    Stack<T>& operator=(Stack<T2> const&);
}

template<typename T>
template<typename T2>
Stack<T>& Stack<T>::operator=(Stack<T2> const& op2)
{
    if((void*)this == (void*)&op2){
        return *this;
    }
    
    Stack<T2> temp(op2);
    elems.clear();
    while(!temp.empty()){
        elems.push_front(temp.top());
        temp.pop();
    }
    return *this;
}

模板作为模板参数

// typename T1 = allocator<T>> class CONT是模板参数,但它也是一个模板,缺省值是std::deque模板,CONT和缺省值std::deque两个模板的模板参数必须完全匹配。
template<typename T, template<typename ELEM, typename T1 = allocator<ELEM>> class CONT = std::deque>
class Stack
{
    CONT<T> elems;
}

零初始化

任何未被初始化的局部变量都具有一个不确定的(undefined)值。

模板参数的初始化方式:

template<typename T>
class MyClass
{
    T x;
    MyClass(): x(){}	// 初始化方法1
    void foo()
    {
        x = T();  // 初始化方法2
    }
}

对于内建类型,比如int,bool等,也可以采用这种方法初始化。

这两种初始化方式是通用的,指针也可以这样初始化,初始化为0。

使用字符串作用函数模板的实参

字符常量的类型为const char[]。比如“name”的类型为const char[5],5 = 4个字符 + 一个’\0’。

const char[4]和const char[5]是不同的类型,它门占用的内存大小不相同。

// 使用引用时,字符串常量模板实参为其本身,即字符常量数组const char[]
template<typename T>
inline T const& max(T const& a, T const& b)
{
	return a < b ? b : a;    
}

string s;
::max("apple", "peach");  // OK:相同类型的实参,都为const char[6]
::max("apple", "tomato"); // ERROR:不同类型的实参,const char[6], const char[7]
::max("apple", s);        // ERROR:不同类型的实参,const char[6], string

========================================================================================

// 不使用引用时,字符串常量作为参数会退化成const char*指针
template<typename T>
inline T max(T a, T b)
{
	return a < b ? b : a;    
}

string s;
::max("apple", "peach");  // OK:相同类型的实参,都为const char*
::max("apple", "tomato"); // OK:相同类型的实参,都为const char*
::max("apple", s);        // ERROR:不同类型的实参,const char*, string

包含模式

当我们组织模板代码时,对于模板函数,把模板函数的声明放在.h文件,模板函数的定义放在.cpp文件;对于模板类,将模板类的定义放在.h文件,成员函数的定义放在.cpp文件,编译器编译时会报错。

// xx.h
template<typename T>
inline T const& max(T const& a, T const& b);	// 模板函数的声明

template<typename T>
class MyClass							// 模板类的定义
{
public:
    void Foo();						    // 成员函数的声明
}

// xx.cpp
template<typename T>
inline T const& max(T const& a, T const& b)     // 模板函数的定义
{
    return a < b ? b : a;
}

template<typename T>
void MyClass<T>::Foo()					// 成员函数的定义
{
    
}

// main.cpp
#include<xx.h>

int main()
{
    max(2, 3);					// ERROR:无法解析外部符号max<int>()
    
    MyClass<int> myClass;
    myClass.Foo();				// ERROR:无法解析外部符号MyClass<int>::Foo()
    
    return 0;
}

首先,这个报错发生在链接阶段。在编译阶段,调用max()和MyClass::Foo()时,编译器只看到了它们的声明,但编译依然会通过。编译器会生成一个指向定义的引用,让链接器在链接的时候将引用指向定义。但是cpp文件作为一个编译单元是互相独立的,就是xx.cpp文件编译时,编译器不知道main.cpp文件中对max()和MyClass::Foo()的调用,也就不会实例化Foo()和MyClass,也就不会生成max()和MyClass::Foo()的定义,这样链接器链接的时候就找不到定义。

包含模型是对上面问题的一个解决方案,将模板的声明和定义都放在.h文件。c++自带的头文件和STL就是采用的这种方式。

// xx.h
template<typename T>
inline T const& max(T const& a, T const& b);	// 模板函数的声明

template<typename T>
void MyClass<T>::Foo()					// 成员函数的定义
{
    
}

template<typename T>
class MyClass;							// 模板类的声明

template<typename T>
class MyClass							// 模板类的定义
{
public:
    void Foo(){}					    // 成员函数的声明和定义
    void Print();						// 成员函数的声明
}

template<typename T>					// 成员函数的定义
void Print() 
{
    
}

// 类的成员函数默认都是内联的,如果将成员函数的定义放到.cpp文件就不是内联的了。

如果有多个cpp文件包含xx.h,如果每个cpp文件都会对xx.h中的模板进行max()的实例化,这样max()不是有多个定义了吗?不会产生多个定义,c++链接时会进行优化。但是特别是当包含c++的头文件时(比如等),由于模板,编译器会产生大量的模板实例代码,很大的增加了编译的复杂程度,使编译的时间也大量增加(每个文件都会编译大量相同的模板实例)。对于这个问题也有解决方案:

  1. 分离模型,并不是所有的编译器都支持
  2. 预编译头文件

静多态

一般,使用虚函数和继承实现的是动多态,即在运行期间确定调用者的类型。使用模板,可以实现静多态,在编译期间确定调用者的类型。

例如我们要对某一类对象进行统一处理,使用虚函数可以这样实现:

class BaseType
{
public:
	virtual void action1(){...}
    virtual void action2(){...}
}

class SubType1 : BaseType
{
public:
    virtual void action1(){...}
    virtual void action2(){...}
}

class SubType2 : BaseType
{
public:
    virtual void action1(){...}
    virtual void action2(){...}
}

// 统一处理
void DoAction1(BaseType const& type)
{
    type.action1();
}

void DoAction2(BaseType* type)
{
    type->action2();
}

// 可以处理异类集合
void DoActions(std::vector<BaseType*> list) 
{
    ...
}

如果使用模板来实现,就会变成这样

class SubType1
{
public:
    void action1(){...}
    void action2(){...}
}

class SubType2
{
public:
    void action1(){...}
    void action2(){...}
}

// 统一处理
template<typename BaseType>
void DoAction1(BaseType const& type)
{
    type.action1();
}

template<typename BaseType>
void DoAction2(BaseType* type)
{
    type->action2();
}

// 不可以处理异类集合,因为要在编译期间确定,vector只能为一个类型的集合
template<typename BaseType>
void DoActions(std::vector<BaseType*> list)   // 处理异类集合会报错
{
    ...
}

动多态和静多态两者的优点

动多态:

  1. 可以处理异类集合
  2. 生成的代码比较小,只需要一个多态函数,而静多态会生成多个实例化函数

静多态:

  1. 不需要和公共基类绑定,可以自由实现处理类型
  2. 代码运行效率更高,直接调用函数,比虚函数间接调用拥有更多内联的机会

使用模板实现Bridge Pattern

用一个指针引用具体的实现,然后把所有的调用都委托给这个(包含这个指针)的类。

使用多态实现:

// 实现的接口基类
class Implementation
{
	virtual operationA() = 0;
    virtual operationB() = 0;
    virtual operationC() = 0;
}

// 实现A
class ImplementationA : Implementation
{
    virtual operationA(){...}
    virtual operationB(){...}
    virtual operationC(){...}
}

// 实现B
class ImplementationB : Implementation
{
    virtual operationA(){...}
    virtual operationB(){...}
    virtual operationC(){...}
}

class Bridge
{
public:
    // 通过将body指向不同的实现类来调用不同的实现
    Implementation* body;
    
    void operationA()
    {
        body->operationA();
    }
    
    void operationB()
    {
        body->operationB();
    }
}

使用模板实现,即使用静多态:

// 实现A
class ImplementationA : Implementation
{
    virtual operationA(){...}
    virtual operationB(){...}
    virtual operationC(){...}
}

// 实现B
class ImplementationB : Implementation
{
    virtual operationA(){...}
    virtual operationB(){...}
    virtual operationC(){...}
}

template<typename Implementation>
class Bridge
{
public:
    // 使用不同的类型来生成不同实现的实例化类
    Implementation* body;
    
    void operationA()
    {
        body->operationA();
    }
    
    void operationB()
    {
        body->operationB();
    }   
}

使用模板实现桥接模式的一个例子就是C++STL的迭代器,通过迭代器间接执行元素的操作。在容器扩容的时候,有时会重新申请一块内存,并将原来的数据转移过去,如果外面有指针指向容器内的元素,那么这个指针就会失效,如果是指向迭代器,那么就不会有问题。

template<typename T>
class Iterator
{
private:
	T value;
public:
    T operator+(T const& right){...}
    ...
}

http://www.kler.cn/a/463708.html

相关文章:

  • 给vscode的新项目选择虚拟环境
  • 1961-2022年中国大陆多干旱指数数据集(SPI/SPEI/EDDI/PDSI/SC-PDSI/VPD)
  • vue cli更新遇到的问题(vue -V查询版本号不变的问题)
  • epoll 水平ET跟边缘LT触发的区别是什么
  • 浏览器选中文字样式
  • 重装操作系统后 Oracle 11g 数据库数据还原
  • MyBatis执行一条sql语句的流程(源码解析)
  • 深度学习——损失函数汇总
  • 【第四期书生大模型实战营基础岛】L1G5000——XTuner 微调个人小助手认知任务
  • 工业相机基本参数
  • Redis——主从复制模式
  • 一道C++面试题关于nullptr的知识点
  • Echarts+vue电商平台数据可视化——webSocket改造项目
  • ES数据管理
  • UnityRenderStreaming使用记录(三)
  • 远程服务器训练模型,使用tmux防止训练终止
  • Go语言方法和接收器类型详解
  • vue3配置测试环境、开发环境、生产环境
  • UNI-APP_i18n国际化引入
  • 无人机技术突飞猛进,超大集群飞行技术详解
  • http报头解析
  • 爬虫与反爬虫实现全流程
  • STM32-I2C通信
  • 安装PostgreSQL后的初始化操作
  • 浏览器--解决页面没刷新的问题(清除所有缓存)
  • 【从零开始入门unity游戏开发之——C#篇35】C#自定义类实现Sort自定义排序