C++模板编程基本概念

C++模板编程基本概念

1. 模板概念

  模板从本质来说是对类型的一种抽象,我们在编程过程中会遇到很多代码除了类型差异以外其他完全一样,比如两类型相同数相加的函数、插入排序算法,这种类似的场景代码实现结构是完全相同的,除了被操作对象的类型不一样,此时,模板便派上用场,从而让我们避免重复造轮子的过程。

// 1. 非模板实现方式
int Add(int nA, int nB)
{
	return nA + nB;
}

float Add(float fA, float fB)
{
	return fA + fB;
}

double Add(double dA, double dB)
{
	return dA + dB;
}

// ... 如果有unsigned int,你还得继续写,继续搬砖,你看这就是你加班的原因

// 2. 模板实现方式
template <typename T>
T Add(T a, T b)
{
	return a + b;
}

// 使用模板,我们就只要定义一个模板函数就全给搞定了,你看这多简单

  同样,我们来看用模板实现一个插入排序:

// 1. 非模板方式实现
void InsertSort(vector<int>& vecData)
{
    if (vecData.size() <= 1)
        return;

    for (size_t i = 1; i != vecData.size(); ++i)
    {
        int nIndex = static_cast<int>(i) - 1;
        int nValue = vecData[i];

        while (nIndex >= 0 && (nValue < vecData[nIndex]))
        {
            vecData[nIndex + 1] = vecData[nIndex];
            --nIndex;
        }

        vecData[nIndex + 1] = nValue;
    }
}

// 还要实现 float,double 等等
void InsertSort(vector<float>& vecData);
void InsertSort(vector<double>& vecData)

// 2. 模板方式实现
template <typename T>
void InsertSort(vector<T>& vecData)
{
    if (vecData.size() <= 1)
        return;

    for (size_t i = 1; i != vecData.size(); ++i)
    {
        int nIndex = static_cast<int>(i) - 1;
        T value = vecData[i];

        while (nIndex >= 0 && (value < vecData[nIndex]))
        {
            vecData[nIndex + 1] = vecData[nIndex];
            --nIndex;
        }

        vecData[nIndex + 1] = value;
    }
}

  通过使用模板,我们可以做到1次实现,多类型受用的好处。

2. 模板实例化

  模板不会被编译成可以处理任何类型的单一实体,而是对于使用模板的每种类型,都会生成不同的实体,而这个生成实体的过程被称为实例化,实例化是根据模板的定义为具体的类型生成实际的代码,这样描述可能有点抽象,比如我们定义了上述的两数相加的模板函数,接下来,我需要在某个函数中使用这个模板函数:

// 模板函数定义
template <typename T>
T Add(T a, T b)
{
	return a + b;
}

// 模板函数使用
void Test()
{
	int nA = 10;
	int nB = 20;
	int nSum = Add(nA, nB);
}

当我们在使用Add函数时,编译器会根据参数推导出我们需要一个int类型的加法函数,从而为我们实例化出一个int类型的实体函数:

int Add(int nA, int nB)
{
	return nA + nB;
}

  模板实例化又分为两种:

  • 隐式实例化
  • 显式实例化

2.1 隐式实例化

  在代码中实际使用模板类构造对象或者调用模板函数时,编译器会根据调用者传给模板的实参进行模板类型推导然后自动生成具体类型的实例或函数实例。

// 类模板
template <typename T>
class Stack
{

private:
    vector<T> m_vecT;
};

// 函数模板
template <typename T>
T Add(T a, T b)
{
	return a + b;
}

void Test()
{
	Stack<double> stackData;	// 隐式实例化:class Stack{ private: vector<double> m_vecT;};
	int nSum = Add(10, 10);		//隐式实例化,int Add(int nA, int nB);
}

  隐式实例化特点:

  • 编译器根据调用时的模板参数类型自动实例化模板。
  • 只实例化使用到的模板代码,不会生成多余的实例。

2.2 显式实例化

  显式实例化是指在代码中通过显式语法强制实例化模板,使得模板为特定类型生成实例,显示实例化语法如下:

template 函数签名<具体类型>;  			// 显式实例化函数模板
template class 类模板名<具体类型>; 		// 显式实例化类模板

template int Add<int>(int, int);
template class Person<int>;

  显式实例化特点:

  • 显式实例化用于提前生成模板实例,即使它们没有在当前代码中直接使用;
  • 常用于将模板定义与实现分离;

  第一个特点很好理解,对于第二个特点,比如当使用函数模板触发其实例化的时候,编译器(在某个时间点)需要查看模板的定义,常规的C++编程来说,我们一般把声明和定义放在头文件和源文件隔离开来,如果在模板编程中,也将声明和定义至于头文件和源文件隔离开来,那么隐式实例化时,我们将找不到其定义,也就是说当模板的实现与定义分离时,隐式实例化可能导致链接错误。
  如果声明和定义全放在头文件中,那么又会出现重定义错误:

// Person.h
template<typename T>
class Person
{
public:
    T m_data;
};
 
// test1.cpp
#include "Person.h"
Person<int> t1;
Person<int> t2;
 
//test2.cpp
#include "Person.h"
Person<int> t3;			// 出现重定义
Person<int> t4;			// 出现重定义

好在现在我们的编译器帮我们处理了重定义错误,所以我们在学习模板编程时,很多书籍都建议我们采用相对简单的方式:在头文件中实现每个模板。至于想了解编译器是怎么处理重定义的,可以见下一段描述,对此不感兴趣的可以跳过。
  Borland模式:Borland模式通过在编译器中加入与公共块等效的代码来解决模板实例化问题。在编译时,每个文件独立编译,遇到模板或者模板的实例化都不加选择地直接编译。在链接的时候将所有目标文件中的模板定义和实例化都收集起来,根据需要只保留一个。这种方法实现简单,但因为模板代码被重复编译,增加了编译时间。在这种模式下,我们编写代码应该尽量让模板的所有定义都放入头文件中,以确保模板能够被顺利地实例化。要支持此模式,编译器厂商必须更换支持此模式的链接器。由于我们为每一份实例化生成代码,这样在大型程序中就有可能包含很多重复的实例化定义代码,虽然链接阶段,链接器会剔除这些重复的定义,但仍然会导致编译过程中的目标文件(或者共享库文件)过于庞大。
  我们还可以通过模板显式实例化的方法解决隔离导致的链接错误。

  • 不用显式实例化
// .h文件
#ifndef ADD_H
#define ADD_H

template <typename T>
T Add(T a, T b);

#endif // ADD_H

// .cpp文件
#include "Add.h"
template <typename T>
T Add(T a, T b)
{
    return a + b;
}

// 调用模板函数
void Test()
{
    int nSum = Add(10, 10);				// 隐式实例化模板
    qDebug() << "Sum = " << nSum;
}

编译时出现报错:
在这里插入图片描述

  • 使用显式实例化
// .h文件
#ifndef ADD_H
#define ADD_H

template <typename T>
T Add(T a, T b);
extern template int Add(int, int);		// 显式实例化声明

#endif // ADD_H

// .cpp文件
#include "Add.h"
template <typename T>
T Add(T a, T b)
{
    return a + b;
}
template int Add(int, int); 			// 显式实例化

// 调用模板函数
void Test()
{
    int nSum = Add(10, 10);
    qDebug() << "Sum = " << nSum;
}

无报错,正常输出
在这里插入图片描述
总的来说,模板显式实例化很像C语言去定义一个全局变量的方式。

// 全局变量定义在.cpp文件中
#include "Global.h"

int g_nDataA = 10;
int g_nDataB = 20;
int g_nDataC = 30;

// .h文件中
#ifndef GLOBAL_H
#define GLOBAL_H

extern int g_nDataA;
extern int g_nDataB;
extern int g_nDataC;

#endif // GLOBAL_H

// 使用全局变量
void Test()
{
    qDebug() << "g_nDataA = " << g_nDataA;
    qDebug() << "g_nDataB = " << g_nDataB;
    qDebug() << "g_nDataC = " << g_nDataC;
}

3. 模板参数推导

  只有函数模板支持模板参数的自动推导,编译器根据给出的实参来推导出T的类型(一定是非引用类型);

  • T&:T的左值引用,可以绑定到一个Obj的左值上,不论const性,在const左值的情况下被推导为const Obj;
  • const T&:T的const左值引用,可以绑定到任意Obj上,左值或右值都可以。
  • T&&:T的转发引用,可以绑定到任意Obj上,左值或右值都可以。

  有一点值得注意,在C语言中使用数组传参会退化为指针,同样,为了兼容C语言,在C++中对于值传参仍然会有数组退化为指针的行为,但是使用引用时,我们可以保留数组完整的类型信息,我们看如下实例:

template <typename T>
struct PrintClass;

template <typename T>
struct PrintClass<T[]>
{
    static void print()
    {
        qDebug() << "print() for T[]";
    }
};

template <typename T, std::size_t SZ>
struct PrintClass<T[SZ]>
{
    static void print()
    {
        qDebug() << "print() for T[" << SZ << "]";
    }
};

template <typename T>
struct PrintClass<T(&)[]>
{
    static void print()
    {
        qDebug() << "print() for T(&)[]";
    }
};

template <typename T, std::size_t SZ>
struct PrintClass<T(&)[SZ]>
{
    static void print()
    {
        qDebug() << "print() for T(&)[" << SZ << "]";
    }
};

template <typename T>
struct PrintClass<T*>
{
    static void print()
    {
        qDebug() << "print() for T*";
    }
};

template <typename T1, typename T2, typename T3>
void Foo(int arrA[], int arrB[7], int(&arrC)[], int(&arrD)[7], T1 t1, T2& t2, T3&& t3)
{
    PrintClass<decltype(arrA)>::print();			// "print() for T*"  数组退化为指针
    PrintClass<decltype(arrB)>::print();			// "print() for T*"  数组退化为指针
    PrintClass<decltype(arrC)>::print();			// "print() for T(&)[]"
    PrintClass<decltype(arrD)>::print();			// "print() for T(&)[7]"
    PrintClass<decltype(t1)>::print();				// "print() for T*"  数组退化为指针
    PrintClass<decltype(t2)>::print();				// "print() for T(&)[]"
    PrintClass<decltype(t3)>::print();				// "print() for T(&)[]"
}


void Test()
{
    int arrDataA[7] = { 1, 2, 3, 4, 5, 6, 7 };
    extern int arrDataB[];

     PrintClass<decltype(arrDataA)>::print();			// "print() for T[7]"
     PrintClass<decltype(arrDataB)>::print();			// "print() for T[]"

     Foo(arrDataA, arrDataA, arrDataB, arrDataA, arrDataB, arrDataB, arrDataB);
}

int arrDataB[] = { 1, 2, 3, 4, 5, 6, 7 };

我们发现当采用普通值传递时,数组会退化为指针类型,采用引用时,会保留数组的所有类型信息,也就是类型和数组长度。
在这里插入图片描述
再看一个示例,基于STL实现一个容器的遍历模板函数,容器包含数组,需要对于数组独立实现一份。

// STL容器
template <typename T>
void Iterator(const T& t)
{
    for (typename T::const_iterator it = t.begin(); it != t.end(); ++it)
        qDebug() << "Value = " << *it;
}

// 一般数组
template <typename T, size_t N>
void Iterator(const T(&arr)[N])
{
    typedef const T* ptr_t;
    for (ptr_t p = arr; p != arr + N; ++p)
        qDebug() << "Value = " << *p;
}

void Test()
{
    int arr[7] = { 1, 2, 3, 4, 5, 6, 7 };
    Iterator(arr);

    vector<int> vecData{ 1, 2, 3, 4, 5, 6, 7 };
    Iterator(vecData);
}

改进版,一个能兼容数组遍历的统一模板函数:

template <typename T>
void Iterator(const T& t)
{
    using std::begin;
    using std::end;

    for (auto p = begin(t); p != end(t); ++p)
        qDebug() << "Value = " << *p;
}

void Test()
{
    int arr[7] = { 1, 2, 3, 4, 5, 6, 7 };
    Iterator(arr);

    vector<int> vecData{ 1, 2, 3, 4, 5, 6, 7 };
    Iterator(vecData);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值