模板编程是一种静态多态,它发生在编译期 ,另外函数重载也是一种静态多态,因此我们也称函数重载为

函数多态。

这里需要说明的一点,任何一种技术,我们都不能滥用,简单的说不能为了用而用,我们需要清楚,什么场景下,可以使用模板,

什么场景下,可以考虑使用动态多态(虚函数多态)。

例如,我以前一个同事在一个业务类中写了一个这样的模板函数:

template<int index>

int getObjectByIndex(){

std::vector<int> contains; //原本是一个类成员容器,返回也不是int, 这里主要想表达一下它的意思。

return contains[index];

}

大致的代码如上,这就是典型的错误应用。

模板编程更适合用于封装一些工具类,而不是应用在业务类中(可以在业务类中使用模板,不宜在其中编写)。

至于动态多态与静态多态他们区别和优劣,这里就不累述。

模板编程常用关键字

template、typename、class、decltype、static_assert、constexpr、using等。

C++模板分类

分为3类:函数模板、类模板、常量模板

函数模板

常用范式为:

template<typename T>

void func(T t){}

或者:

template<class T>

void func(T t){}  //模板中的typename都可以换成class,后续统一全部写成typename。

类模板

常用范式为:

template<typename T>

class vector{};

常量模板

常用方式为:

template<typename T, typename U>

static constexpr bool is_same_v = std::is_same<T,U>::value;

常量模板是Traits的一种扩展。

模板参数的书写范式

template<typename T = DefaultT, …>

其中T为模板参数,DefaultT为缺省模板参数,除了类型模板还有整型模板:

template<int Val = 0>

整形模板类型有很多,例如: bool、char、short、unsigned int等等,整形模板比较简单,

和类型模板有些相似,后续就不针对整型模板讲解。

类型模板的范式还可以写成:

template<typename T>   //常见写法。

template<typename = DefaultT>  // 通常用与模板鉴定, 后续解释

template<typename>     //用于声明

而其他的语法都是错误,例如:

template<typename T = >

template< = 0>

看着就别扭,当然是错误的,但是错误的,也有大用处哈,后续会讲解到。

模板参数包的书写范式:

template<typename …Args>

模板模板的书写范式:

template<typename template<typename…> T, …>

using 关键字

using关键字,大家应该都很熟悉,使用命名空间嘛,但它的用法还有很多:

1.使用命名空间

using namespace std;

2. 术语不记得了,参考如下源码

class MyObject{

public:

MyObject(int);

MyObject(QPoint, QPoint);

protected:

void func(int);

void func(QRect rect):

};

这里继承一下。

class NewObject : public MyObject{

public:

NewObject(int a):MyObject(a){}

NewObject(QPoint p1, QPoint p2):MyObject(p1, p2){}

public: // 下面这种写法是为了将基类的私有方法提成共有的,很少见哈。

void func(int a){ __super::func(a);}

void func(QRect rect) {__super::func(rect);}

};

写起来很繁琐,都是调用基类的方法。我们可以用using关键字简化书写。

class NewObject : public MyObject{

public:

using MyObject::MyObject;

using MyObject::func;

};

这种写法,等同于上面的写法。

3.  类型定义

例如: using Int = int;  <==> typedef int Int;

但是using的功能比typedef要强大,还可以用于模板类型定义。例如:

template<typename T>

using MyVector = std::vector<T>;

MyVector区别于std::vector,MyVector只有一个模板参数,而std::vector有两个,最后一个模板类型有缺省参数而已。

typename关键字

typename直译为类型名,常用于模板的参数指定,除了这个用法,它还有其他的用法,假定模板参数有内置类型。

例如1:

template<typename Container,  tpyename Type = typename Container::type>

Type back_elem(Container& t){}

标准的容器都内置定义元素类型,例如: std::vector<int>::type == >   int

例如2:

template<typename Functor>

void func(typename QtPrivate::FunctionPointer<Functor>::Object* object, Functor functor){}

虽然Object是在FunctorPointer中定义的,这里仍然需要typename,因为模板还有特化,因此这里仍然是假定该类型有某个内置类型。

省略号(…)

省略号在C++中有3种用法

不定参数

void func(int num, …);

我们常用printf就是不定参数函数。

不定宏

#define Log(…) printf(__VA_ARGS__)

#define SendEvent(EventType, …) Sender<EventType>().send(__VA_ARGS__)

模板参数包

template<typename …Args>

void func(Args…args){

int a[] = { ((GlobalStream << args), 0) …};

}

当你进行如下调用时, func(1, 3.0), 扩展开是

func<int, double>(int a, double b){

int a[] = { ((GlobalStream << a), 0), ((GlobalStream << b), 0} };

}

模板参数包的函数内部或者类的内部使用,除了直接写缩略号,还可以使用模板递归,更多细节,自己琢磨。

进阶篇

有了上诉的一些基础知识,我们可以尝试了解一些进阶的知识。分别基于类模板和函数模板讲解,其中常量模板是类模板的扩展,因此

就放在类模板中讲解。

类模板

在C++中,类模板一般用的都是struct关键字,而非class。

类模板在Traits中,有两个重要用法,内置类型和内置整形常量,并使用特化使得其变得花里胡哨。

最常用模板pair的定义如下:

template<typename T, typename U>

struct pair{

using First = T;

using Second = U;

First  first;

Second second;

};

模板参数中的类型,是没法在类外去获取的,而重定义成内置类型之后就可以了。

例如:

std:: pair<int, double>::T; //错误

std:: pair<int, double>::First; //正确 <==> int

特化与内置常量

例如:std::is_same的实现如下:

template<typename T, typename U>

struct is_same{

static constexpr bool value = false;

};

特化版本:

template<typename T>

struct is_same<T, T>{

static constexpr bool value = true;

}

那么:

is_same<int, double>::value  ==> false;

is_same<int, int>::value ==> true;

为了简写,定义模板常量如下:

template<typename T, typename U>

static consexpr bool is_same_v = is_same<T, U>::value;

那么:

is_same_v<int, double> ==> is_same<int, double>::value ==> false;

is_same_v<int, int> ==> is_same<int, int>::value ==> true;

另外,我们还可以先定义常量模板,再定义类模板:

template<typename T, typename U>

static constexpr bool is_same_v = false;

// 特化版本

template <typename T>

static constexpr bool is_same_v<T, T> = true;

//类模板

template<typename T, typename U>

struct is_same{

static constexpr bool value = is_same_v<T, U>;

};

这里和标准库有点出入,可以参考标准库 std::true_type和 std::false_type的定义,挺简单,这里就不细说了。

特化与内置类型

我们参考remove_const的使用

template<typename T>

struct remove_const{

using type = T;

};

template<typename T>

struct remove_const<const T>{

using type = T;

};

使用时:

remove_const<int>::type ==> int;

remove_const<const int>::type ==> int;

为了方便使用,可以做如下定义:

template<typename T>

using remove_const_t = remove_const<T>::type;

那么:

remove_const_t<int> ==> remove_const<int>::type ==> int

remove_const_t<const int> == > remove_const<const int>::type ==> int

从这里可以看出常量模板其实就源于内置类型的推广, 一个是扩展_v, 一个是扩展_t。

举一个例子:

template<typename T>

void func(T t)
{

std::string str = t;

};

这里例子有点模糊,真实的例子又太复杂,这里假定传入一个模板参数T, 内部用时,用于构建一个字符串。

那么:

func(std::string(“abc”));  //正确

const char* p = “abc”;

func(p); //正确

func(“abc”); //错误, 你们知道为什么吗?

而写成:

func<const char*>(“abc”);  //正确,这是因为”abc”的类型是char[] 类型,std::string可没有这种类型的构造函数。

所以我们可以强制将char[]转成const char*类型, 我们将原函数写成:

template<typename T>

void func(T t){

std::string str = std::decay<T>(t);

}

可以去标准库去看一下 std::decay的定义就知道,它可以将char[]类型转成 char*类型,限于篇幅,这里不详细讲。

偏特化和全特化

上面的例子都是偏特化,所谓偏特化和全特化,区别在于有没有将所有的模板参数都指定。

例如:

template<typename T>

class MyClass {

}

//偏特化版本, 一个参数也能偏特化

template<typename T>

class MyClass<std::vector<T>>{};

//甚至还可以通过偏特化增加模板参数

template<typename T, typename U>

class MyClass<std:: pair<T, U>>{};

另外如果两个特化版本有冲突,会报错的哈。

例如:

template<typename T, typename U>

class A{};

template<typename T>

class A<T, T>{};

template<typename T>

class A<double, T>{};

那么在使用时:

A<double, double> 就有冲突,他不知道用哪个特化版本了。

如果你又写出全特化版本:

template<>

class A<double, double>{};  //冲突又解决了,

模板特化的优先使用规则:

优先使用全特化版本,如果不符(使用之后有语法错误,或者模板参数直接就不符)。

使用偏特化版本,再不符合,使用原始版本。

值得注意的是,原始版本只是低优先级匹配版本,并非不符合的版本。

函数模板

函数模板其实和类模板即为相似,最主要的区别,在于函数模板没有特化,这么说也不准确,没有偏特化,有全特化。

但是即便是全特化,也没有意义,知道为啥函数没有偏特化吗?

自动推导+函数重载

函数有重载的概念,函数重载,是唯一不用模板也能做到动态多态的一种方式,因为有了函数重载,就

不需要特化了, 没有特化,就没有特化的优先级规则,但是却有函数重载匹配优先级规则。

例如, 下面有4个函数,形成重载:

void func(double a);     —版本1

void func(dobuel a, int b = 0); //版本1.1

void func(int a);            —版本2

void func(…);                 —版本3

那么如下调用时:

func(3.0);

其中版本1和版本1.1有冲突,我们去掉版本1.1.

那么这剩下的3个版本的重载函数,它都能匹配上,调用谁呢?调用的是最精确的版本,版本1,

版本1没有?调用版本2,如果第二个也没有呢?调用第三个。

我们再加上一个模板版本:

template<typename T>

void func(T t);

func(3.0);

仍然调用的第一个,删除版本1,则直接调用模板版本。

后续我们都假定没有第一个版本, 最精准的版本。

如果我们对T进行限制double类型,写成:

template<typename T, typename = std::enable_if_t<!std::is_same_v<T, double>>>

void func(T t);

你会发现,它又调回第二个版本。

我们再加一个模板函数,反过来只要double类型:

template<typename T, typename = std::enable_if_t<std::is_same_v<double, T>>>
void func(T t);

这个时候编译报错了,提示两个模板版本有冲突, 我们对第二个稍作修饰。

template<typename T, typename = std::enable_if_t<std::is_same_v<double, T>>>

void func(T t, int a = 0);

这个时候,它又调用了第二个模板版本。值得注意的是,非模板函数这样重载,会报错。

总之我想表达的是,模板重载版本,同样也会受参数的影响,从而有一些优先级匹配,

如果存在两个优先级一样,编译就会报错。

我们回归到第一个模板, 去掉后续的一些模板定义:

template<typename T>

void func(T t);

现在我们写一个特化版本:

template<>

void func<double>(double a);

那么:

func(3.0);   //调用是特化版本,你会发现特化其实与版本1是一样的。

把版本1打开,特化版本与版本并没有冲突,这时又调回版本1。

有点乱七八糟的。

全推导与半推导

所谓推导是指模板函数,在调用时,通过输入的参数,自动推导,我们不需要主动填写模板参数,例如:

template<typename T>

void func(T t);

那么:

func(3.0) ==> func<double>(3.0);  //这个时候,可以不用写double, 因为可以自动推导出来。

那么半推导又是什么呢?如下所示:

template<typename Ret, typename T>

Ret func(T t){

return t;

}

那么:

func<double>(3);  ==>func<double, int>(3);  //其中模板int被自动推导出来。

需要注意的一点是,半推导,只能推导后边的参数。如果将上诉的模板Ret和T对调,就会报错。

返回类型后置

我们在用lambda算子时候,由于lambda没有前置返回值的语法,因此要么靠自动推导,要么写后置返回类型,例如:

auto lambda = [](){ return 3;};

而当我们这样写时,则会报错:

auto l2 = [](bool temp) {

if (temp)

return 1;

return 2.1;

};

因为无法推导返回类型,到底是返回int,还是double呢?这个时候我们就需要强制指定后置返回类型,如下:

auto l2 = [](bool temp) → double{

};

同样,在C++11,普通的函数,类成员函数等等,都增加了返回值后置,不过普通有前置返回类型。

例如:int func();

可以写成 auto func()→int;

至于有啥用,我也不知道,比如这个场景写到后面,好看一些:

template<typename T, typename U>

auto add(T t, U u) → decltype(T() + U()){

return t + u;

}

上面这种写法是有问题的,因为T或者U没有无参构造就会报错,但事实推导过程是发生编译期,压根就不打算用无参构造一个T和U对象。

只不过我们推导时候,需要一个T对象或者U对象,那怎么办呢?

我们可以这么写:

template<typename T, typename U>

auto add(T t, U u) →decltype(std::declval<T>() + std::declval<U>()){

return t + u;

};

std::declval又是个啥玩意呢?定义如下:

template<typename T>

T declval();

这就是它的全部,一个没有实现的模板函数申明,就是它的全部,因为推导过程发生编译期,运行期有不打算构造T对象,所以,懂了吗?

高级篇

语法检测

现在我们假定需要实现一个求余运算。

template<typename T>

T remain(T t, T u){

return t % u;

};

remain(4.3, 2.1); //报错, double类型不支持求余运算,但是double 类型我们可以如下的进行计算。

return t – u * int(t/u);  //float类型也是一样,我们有什么办法,可以直接检测语法是否成立呢?

这个时候,我们用到标准库 std::void_t语法,它的定义如下:

template<class …_Types>
using void_t = void;

一看都不知道这是个啥玩意,跟检测有什么关系呢?

我们可以基于它进行如下实现:

template<typename T, typename = void>

struct CanRemain : std::false_type{};

template<typename T>

struct CanRemain<T, std::void_t<decltype(std::declval<T>()%std::declval<T>())>> : std::true_type{};

解释一下,非特化版本的value为false, 而特化版本的value是true, 但是第二个参数的模板参数是靠推导出来,

如果语法不成立,那么就无法推导一个模板类型出来,推导不出来,不好意思,无法匹配特化版本, 不用

std::void_t也行哈,例如写成:

template<typename T>

struct CanRemain<T, decltype(std::declval<T>()%std::declval<T>())> : std::true_type{};

效果一样,只不过std::void_t是一个模板参数包,可以进行多条语法检测。

继而:

CanRemain<double>::value ==> false, 用的是特化版本

CanRemain<int>::value ==> true, 用的非特化版本

我们可以用上述范式检测大多数语法,例如检测某一个类是否有特定的成员变量、成员函数、内置类型等等。

基于这玩意,我们再次去实现上述的模板函数.

template<typename T>

T remain(T t, T u){

if (CanRemain<T>::value)

return t % u;

return t – u * int(t/u);

}

这么写有问题吗? 当然有,当你输入double时,推导的函数原型如下:

double remain<double>(double t, double u){

if (false)

return t % u;  //虽然永远走不到这,但是语法就是不成立啊。

return  t – u * int(t/u);

}

这个错误,我犯过很多次,常人容易想到的写法,就是这样,考虑我们这些普通人,C++11增加了一个语法,强制让语法是成立的,

写成如下:

template<typename T>

T remain(T t, T u){

if constexpr (CanRemain<T>::value) //注意,括号中必须是常量表达式。

return t%u;

return t – u * int(t/u);

}

当然除了这种写法,我们还有其它的写法,比如:

template<typename T>

struct DefaultRemain{

static T remain(T t, T u){ return t %u;}

};

template<typename T>

struct OtherRemain{

static T remain(T t, T u){ return t – u * int(t/u);}

}

template<typename T>

T remain(T t, T u){

return std::condition_if_t<CanRemain<T>::value, DefaultRemain, OtherRemain>::remain(t, u);

}

显然后面的写法,复杂不知道多少倍。

现在我们知道语法检测的本质是啥,就是特化版本优先匹配,因为检测语法不成立,导致匹配不上,选择

非特化版本,归根接底,用到的是类模板的优先级。

而函数模板也有优先级的,是不是也可以从函数模板下手,用来检测语法是否成立呢? 当然可以:

下面的代码,就是我从Qt源码中拷贝出来的。

template <typename Functor, typename… ArgList> struct ComputeFunctorArgumentCount<Functor, List<ArgList…>>
{
template <typename D> static D dummy();
template <typename F> static auto test(F f) -> decltype(((f.operator()((dummy<ArgList>())…)), int()));
static char test(…);
enum {
Ok = sizeof(test(dummy<Functor>())) == sizeof(int),
Value = Ok ? int(sizeof…(ArgList)) : int(ComputeFunctorArgumentCountHelper<Functor, List<ArgList…>, Ok>::Value)
};
};

上面的dummy就是std::declval, 这里用到的就是函数重载的优先级,优先匹配的参数完全一致的版本,奈何语法检测不正确,

导致返回类型都推导不出来,继而尝试用低优先级匹配的版本,也就是不定参数的版本。这两个版本返回值类型不一样,因此

通过推导返回值类型,可以获取语法是否成立。

上述这个模板函数,就是推导在指定函数类型是否可以调用传入的指定参数。

模板模板(Template Template)

模板模板是指模板参数仍然是一个模板。例如:

template<typename template<typename> Container>

void backInsertToContainer(Container& con){

std::back_insertor<Container> insertor;

*++insertor = …;

}

上面的函数,大致想做的事情,是想往一个容器中尾部插入一些元素,但是我们不限制用啥容器。例如,如下调用:

std::vector<int> con;

backInsertToContainer(con);

std::list<int> con1;

backInsertToContainer(con1);

模板模板参数的用法,就是这么简单哈,typename template<typename,…>, 值得注意的是,上面的封装是错误的,

或者也可以说上面的封装并不支持下面的调用,知道为什么吗?

番外篇

C++标准库有一个std::is_function的实现,它有诸多的实现版本,但是有一个版本的实现如下:

template<class T> struct is_function  std::integral_constant< bool,

             !std::is_const<const T>::value && !std::is_reference<T>::value >
{};

简单的说一下,所有类型中,加const修饰无效的类型,除了引用类型,就是函数类型,因此修饰无效的类型中,排除掉引用

类型,就一定是函数类型。

那么问题来了,引用类型加const为啥不是const类型,我们不是经常这么写吗?

void output(const std::string& text); 

常引用啊,咋的就引用类型加const修饰无效了呢?

你们知道是为什么吗?

备注: 引用类型还包括右值引用类型。

 

推荐学习链接 : https://en.cppreference.com/w/cpp/language/template_parameters

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。