做网站的教科书,深圳网站建设公司,好看的主题wordpress,wordpress二次元极简主题引言
模板编程#xff0c;指的是可以我们可以将函数或者类的数据类型抽离出来#xff0c;做到类型无关性。我们关注的对象#xff0c;是普通函数、普通类。如下面的这个经典的模板函数#xff1a;
templatetypename T
bool compare(T t1,T t2) {return t1 t2…引言
模板编程指的是可以我们可以将函数或者类的数据类型抽离出来做到类型无关性。我们关注的对象是普通函数、普通类。如下面的这个经典的模板函数
templatetypename T
bool compare(T t1,T t2) {return t1 t2;
}
我们可以使用一份代码来判断两个相同的类型的对象t1是否大于t2。
而模板元编程则是对模板函数、模板类本身进行编程。继续上面的代码例子假如有一些类型他并没有运算符只有运算符那么我们需要重载两个模板函数对这两个类型的数据进行分类
// 函数
templatetypename T
bool compare(T t,T t2) {return t t2;
}
// 函数
templatetypename T
bool compare(T t,T t2) {return t t1;
}拥有运算符的类型进入函数1拥有运算符进入函数2。我们这里对模板类型进行判断、选择的过程就是模板元编程。可以说模板编程是将数据类型从函数或者类抽离出来而模板元编程则是对类型进行更加细致的划分分类别进行处理。
这个时候可能有读者会有疑问这不就是类型识别吗我用typeid也可以实现啊例如以下代码
templatetypename T
void show(T t) {if(typeid(T).hash_code()...) {t.toString();} else {t.toType();}
}这种写法是错误的。上面代码例子中无法通过编译原因是T类型无法同时拥有toString()和toType()函数即使我们的代码只会运行其中一个路径。其次
typeid在多动态库环境下会出现不一致的问题并不是非常可靠。typeid只能对已有的数据类型进行判断无法判断新增类型。会导致函数臃肿判断条件众多代码不够优雅。
原因有很多这里列举了几条一句话总结就是不可靠、不适用、不优雅。因此我们才需要模板元编程。
那么如何在模板中实现对类型的判断并分类处理呢我们接着往下看。
文章内容略长我非常建议你完整阅读但是如果时间比较紧可以选择性阅读章节
开始从一个具体的例子从0到1解析模板元编程
模板函数重载匹配规则模板匹配规则介绍模板编程最核心的两个规则他是整个模板元编程依赖的基础
最后的章节进行全文的总结
开始
我们先从一个例子来看模板元编程是如何工作的。我们创建一个类HasToString其作用是判断一个类型是否有toString成员函数使用的代码如下
templatetypename T HasToString{...}
class Dog {
};
class Cat {
public:std::string toString() const{return cat;}
};
std::cout Dog: HasToStringDog::value std::endl; // 输出
std::cout Cat: HasToStringCat::value std::endl; // 输出
通过类HasToString我们可以判断一个类型是否有toString这个成员函数。好接下来让我们看一下HasToString是如何实现的
// 判断一个类型是否有 toString 成员函数
templatetypename T
class HasToString {templatetypename Y, Y yclass Helper {};templatetypename U Tconstexpr static bool hasToString(...) {return false;}templatetypename U Tconstexpr static bool hasToString(Helperstd::string (U::*)() const,U::toString*) {return true;}
public:const static bool value hasToStringT(nullptr);
};
好家伙这也太复杂了完全没看懂。你是否有这样的感觉呢如果你是第一次接触感觉比较复杂很正常现在我们无需完全理解他下面我们一步步慢慢说。
首先有两个c的其他知识先解释一下constexpr关键字和成员函数指针了解的读者可以直接跳过。
constexpr表示一个变量或者函数为编译期常量在编译的时候可以确定其值或者函数的返回值。在上面的代码中const static bool value 需要在编译器确定其值否则不能在类中直接复制。因此我们给hasToString函数增加了constexpr关键字。
成员函数指针我们可以获取一个对象的成员函数指针而在合适的时候调用此函数。如下代码
std::string (Cat::*p)() const Cat::toString; // 获取Cat的函数成员指针
Cat c;
std::string value (c.*p)(); // 通过成员函数指针调用c的成员函数
可以看到成员函数指针的声明语法和函数指针很相似只是在前面多了Cat::表示是哪个类的指针。
这里仅简单介绍其他更详细的内容感兴趣可以百度一下了解。
好我们第一步先看到HasToString的value变量他是一个const static bool类型表示T类型是否有toString函数的结果。他的值来源于hasToStringT(nullptr)我们继续看到这个函数。
hasToString是一个返回值为bool类型的模板函数由于其为constexpr static类型使得其返回值可以直接赋值给value。他有两个重载实例
第一个重载函数的参数为函数参数包第二个重载函数的参数为Helper对象的的指针
我们暂时先不管Helper的内容当我们调用hasToStringT(nullptr)时他会选择哪个重载函数答案是不管T类型如何都会先进入第二个重载函数。原因是第二个重载函数相比第一个更加特例化实参与形参均为指针类型根据模板函数匹配规则他的优先级更高因此会选择第二个重载函数进行匹配。
到这里我们已经可以明确在编译时不管T的类型如何均会调用到hasToString的第二个重载函数。这个时候我们看到模板类Helper他的模板类型很简单第一个模板参数是Y而第二个模板参数则为第一个模板类型的对象值。
看到hasToString第二个重载函数其参数为一个Helper类型指针。其中Helper的第一个模板类型描述了成员函数toString的函数类型第二个模板参数获取模板类型U的成员函数toString的指针。这一步可以保证类型U拥有成员函数toString,且类型为我们所描述的函数类型。
好到这里就可能有两种情况
假如类型U拥有toString成员函数那么函数匹配正常hasToString实例化成功。假如类型U没有toString成员函数此时会匹配失败因为U::toString无法通过编译。这个时候根据c的模板匹配规则匹配失败并不会直接导致崩溃而是会继续寻找可能的函数重载。
对于类型Dog他没有toString成员函数hasToString第二个重载函数匹配失败此时会继续寻找hasToString的其他重载类型。到了第一个重载类型匹配成功类型Dog匹配到hasToString第一个重载函数。
这里就是我们整个HasToString的重点他成功将含toString成员函数的类型与不含toString成员函数的类型成功分到两个不同重载函数中去完成我们判断的目的。
这就是模板元编程。
好了对于一开始我们觉得很复杂的代码我们也基本都了解了可以先暂时松一口气先来回顾一下上面的内容
// 判断一个类型是否有 toString 成员函数
templatetypename T
class HasToString {templatetypename Y, Y yclass Helper {};templatetypename U Tconstexpr static bool hasToString(...) {return false;}templatetypename U Tconstexpr static bool hasToString(Helperstd::string (U::*)() const,U::toString*) {return true;}
public:const static bool value hasToStringT(nullptr);
};
我们创建了一个模板类HasToString来判断一个类型是否拥有toString成员函数并将结果存储在静态常量value中。value的值来源于静态模板函数hasToString的判断我们将该函数设置为constexpr类型因此可以直接将返回值赋值给value。利用模板函数重载匹配规则将函数调用优先匹配到hasToString的第二个重载函数进行匹配。我们创建了Helper辅助模板类来描述我们需要的成员函数类型并获取类型的成员函数。利用模板匹配规则匹配失败的类型将进入hasToString的第一个重载函数进行匹配实现类型的选择。
整个过程最核心的部分是模板函数hasToString的重载与匹配。而其所依赖的是我们重复提到模板函数重载匹配规则、模板匹配规则那么接下来我们来聊聊这个匹配规则的内容。
模板函数重载匹配规则
模板函数重载匹配规则他规定着当我们调用一个具有多个重载的模板函数时该选择哪个函数作为我们的调用对象。与普通函数的重载类似但是模板属性会增加一些新的规则。
模板函数重载匹配规则可以引用《c primer》中的一段话来总结
对于一个调用其候选函数包括所有模板实参推断成功的函数模板实例。
候选的函数模板总是可行的因为模板实参推断会排除任何不可行的模板。
与往常一样可行函数模板与非模板按类型转换 如果对此调用需要的话来排序。当然可以用于函数模板调用的类型转换是非常有限的。
与往常一样如果恰有一个函数提供比任何其他函数都更好的匹配则选择此函数。 但是如果有多个函数提供同样好的匹配则
如果同样好的函数中只有一个是非模板函数则选择此函数。如果同样好的函数中没有非模板函数而有多个函数模板且其中一个模板比其他模板更特例化则选择此模板。否则此调用有歧义。
看着有点不知所以然我们一条条来看。这里我给整个过程分为三步
第一步模板函数重载匹配会将所有可行的重载列为候选函数。
举个例子我们现在有以下模板函数以及调用
templatetypename T void show(T t) {...} // 形参为T
templatetypename T void show(T* t) {...} // 形参为T*
int i ;
show(i);
show(i);
代码中模板函数show有两个重载函数其形参不同。当调用show(i)时第一个重载函数T可以匹配为int类型第二重载函数无法完成int类型到指针类型的匹配因此本次调用的候选重载函数只有第一个重载函数。
第二个调用show(i)第一个重载函数T可以匹配为int*类型第二个重载函数T可以匹配为int类型因此本地调用两个重载函数都是候选函数。
选择候选函数是整个匹配过程的第一步过滤掉那些不符合的重载函数再进行后续的精确选择。
第二步候选可行函数按照类型转换进行排序
匹配的过程中可能会发生类型转换需要类型转换的优先级会更低。看下面代码
templatetypename T void show(T* t) {...} // 形参为T*
templatetypename T void show(const T* t) {...} // 形参为const T*
int i ;
show(i);
show两个重载函数均作为候选函数。第一个函数的形参会被匹配为int*而第二个重载函数会被匹配为const int*进行了一次非const指针到const指针的转换。因此前者的优先级会更高。
类型转换主要涉及volatile和const转换上面的例子就是const相关的类型转换。类型转换是匹配过程中的第二步。
此外还有char*到std::string的转换也属于类型转换。字符串字面量如hello属于const char*类型编译器可以完成到std::string的转化。
第三步若第二步存在多个匹配函数非模板函数优先级更高若没有非模板函数则选择特例化更高的函数。
到了这一步基本选择出来的都是精确匹配的函数了。但是却存在多个精确匹配的函数需要按照一定规则进行优先级排序。看下面例子代码
templatetypename T void show(T t) {...} // 形参为T
templatetypename T void show(T* t) {...} // 形参为T*
void show(int i) {...} // 非模板函数
int i ;
show(i);
show(i);
在上面代码中show(i)的调用有两个精确匹配的函数第一个和第三个重载函数。但是第三个重载函数为非模板函数因此其优先级更高选择第三个重载函数。
show(i)调用中可以精确匹配到第一个和第二个重载函数。但是第二个函数相比第一个会更加特例化他描述的形参就是一个指针类型。因此选择第二个重载函数版本。
到此基本就能选择最佳匹配的重载函数版本。若最后出现了多个最佳匹配则本地调用时有歧义的调用失败。
这里需要注意的一点是引用不属于特例化的范畴例如以下的代码在调用时是有歧义的
templatetypename T void show(T t) {...} // 形参为T
templatetypename T void show(T t) {...} // 形参为T
int i ;
show(i); // 调用失败无法确定重载版本
好了这就是整个模板函数重载的匹配过程主要分三步
选择所有可行的候选重载函数版本根据是否需要进行类型转换进行排序优先选择非模板类型函数若无非模板函数则选择更加特例化的模板函数。若出现多个最佳匹配函数则调用失败
了解了模板函数重载的匹配过程那么我们就能在进行模板元编程的时候对整体的匹配过程有把握。除了模板函数重载匹配规则还有一个重要的规则需要介绍模板匹配规则。
模板匹配规则
模板有两种类型模板函数和模板类。模板类没有和模板函数一样的重载过程且在使用模板类时需要指定其模板类型因此其貌似也不存在匹配过程不其实也存在一种场景具有类似的过程默认模板参数。看下面的例子
templatetypename T,typename U int
struct Animal {};
templatetypename T
struct AnimalT,int {};
Animalint animal;
模板类Animal有两个模板参数第二个模板参数的默认类型为int。代码中特例化了T,int类型与第二个模板参数的默认值保持一致。当我们使用Animalint实例化时Animal两个模板参数被转化为int,int模板匹配会选择特例化的版本也就是templatetypename T struct AnimalT,int版本。这个过程有点类似我们前面的模板函数重载匹配过程但是本质上是不同的模板类的匹配过程不涉及类型转换完全是精确类型匹配。但在行为表现上有点类似因此在这里补充说明一下。
这里我们要介绍一个更加重要的规则SFINAE法则。
这个法则很简单模板替换导致无效代码并不会直接抛出错误而是继续寻找合适的重载。我们还是通过一个例子来理解
// 判断一个类型是否有 toString 成员函数
templatetypename T
class HasToString {templatetypename Y, Y yclass Helper {};templatetypename U Tconstexpr static bool hasToString(...) {return false;}templatetypename U Tconstexpr static bool hasToString(Helperstd::string (U::*)() const,U::toString*) {return true;}
public:const static bool value hasToStringT(nullptr);
};
这是我们前面的例子当我们调用hasToStringT(nullptr)时模板函数hasToString的两个重载版本都是精确匹配但是后者为指针类型更加特例化因此优先选择第二个重载版本进行替换。到这里应该是没问题的。
但是如果我们的类型T不含toString成员函数那么在这个部分Helperstd::string (U::*)() const,U::toString会导致替换失败。这个时候按照SFINAE法则替换失败并不会抛出错误而是继续寻找其他合适的重载。在例子中虽然第二个重载版本替换失败了但是第一个重载版本也是精确匹配只是因为优先级没有第二个高这个时候会选择第一个重载版本进行替换。
前面我们在讲模板函数重载规则时提到了候选函数在匹配完成后发生替换失败时会在候选函数中按照优先级依次进行尝试直到匹配到替换成功的函数版本。
这一小节前面提到的模板类的默认模板参数场景也适用SFINAE法则。看下面的例子
class Dog {};
templatetypename T,typename U int
struct Animal {};
templatetypename T
struct AnimalT, decltype(declvalT().toString(),int) {};
AnimalDog animal;
代码中有一个关键字std::declval有些读者可能并不熟悉。
declval的作用是构建某个类型的实例对象但是又不能真正去执行构建过程一般结合decltype使用。例如代码中的例子我们利用declval构建了类型T的实例并调用了其toString的成员函数。使用decltype保证这个过程并不会被执行仅做类型获取或者匹配的过程。更详细的建议读者搜索资料进一步了解declval是c14以后的新特性如果是c11则无法使用。
根据前面的内容我们知道AnimalDog会匹配到特例化的版本但是由于Dog类型没有toString成员函数会导致替换失败。这时候会回到第一个非特例化的版本进行替换。
好了通过这两个例子读者应该也能理解SFINAE法则的内容。模板重载匹配规则是整个模板元编程中最核心的内容利用这个规则就可以在整个匹配的流程的不同的重载中函数重载或者类特例化选择我们需要的类型并将其他不需要的类型根据匹配流程继续寻找匹配的目标从而完成我们对数据类型的选择。
这个过程其实有点类似于流转餐厅厨师放下的食物是数据类型每个客户是重载版本流水线是模板匹配规则流程每个客户选择自己喜爱的食物并将不感兴趣的食物利用流水线往后传每个食物最终都到了感兴趣的客户中。当然如果最终无人感兴趣则意味着匹配出错。
使用
到此我们对于模板元编程核心内容就了解完成了。那么在实际中如何去使用呢这里给出笔者的一些经验。
首先必须要明确目的不要为了使用技术而使用技术。模板元编程能完成的功能是在模板重载中实现对类型的判断与选择。当我们有这个需求的时候可以考虑使用模板元编程这里举几个常见场景。
我们回到我们最开始的那个例子比较大小。假如一个类型拥有操作,采用运算符进行比较否则采用运算符进行比较。这里我们采用默认模板参数的方式进行编写
templatetypename T,typename U int
struct hasOperate {constexpr static bool value false;
};
templatetypename T
struct hasOperateT, decltype(declvalT() declvalT(),int()) {constexpr static bool value true;
};
这样通过value值就可以获取到结果。那么我们很容易写出下面的代码
templatetypename T bool compare(const T t,const T t2) {if(hasOperateT::value) {return t t2;} else {return t t1;}
}
好了大功告成。运行一下诶怎么编译不过这个问题在文章前面有简单提到。对于类型T他可能只有两种操作符其中的一种例如以下类型
class A {
public:explicit A(int num) : _num(num){}bool operator(const A a) const{return _num a._num;}int _num;
};
A类型只有操作符并没有操作符上面的模板函数实例化之后会变成下面的代码
bool compare(const A t,const A t2) {if(hasOperateA::value) {return t t2;} else {return t t1; // 这里报错找不到操作符}
}
代码中即使我们的else逻辑不会运行到但编译器会检查所有关于类型A的调用再抛出找不到操作符的错误。那么我们该如何操作呢有两个思路。
第一个思路是直接在hasOperate结构体中分别编写各自的处理函数。这样能解决一些问题但是局限性比较大不够灵活。
另一个思路就是我要给你介绍的一个非常好用工具类std::enable_if。有了它之后我们可以这么使用
templatetypename T
bool compare(typename std::enable_ifhasOperateT::value,T::type t,T t2) {return t t2;
}
templatetypename T
bool compare(typename std::enable_if!hasOperateT::value,T::type t,T t2) {return t t1;
}
感觉有点不太理解没事我们先来了解一下他。enable_if的实现代码很简单
templatebool enable,typename T
struct enable_if {};
templatetypename T
struct enable_iftrue,T {using type T;
};
他是一个模板结构体第一个参数是一个布尔值第二个是一个泛型T。其特例化了布尔值为true的场景并增加了一个type别名反之如果布尔值为false则没有这个type类型。
回到我们前面使用代码我们使用hasOperateT::value来获取该类型是否拥有指定操作符如果没有则获取不到type类型那么整个替换过程就会失败需要继续寻找其他的重载。这样就实现对类型的选择。
系统库中还提供了很多类型判断接口可以和enable_if一起使用。例如判断一个类型是否为指针std::is_pointer、数组std::is_array等。例如我们可以创建一个通用的析构函数根据是否为数组类型进行析构
templatetypename T void deleteAuto(typename std::enable_ifstd::is_arrayT::value,T::type t) {delete[] t;
}
templatetypename T void deleteAuto(typename std::enable_if!std::is_arrayT::value,T::type t) {delete t;
}
int array[];
int *pointer new int();
deleteAutodecltype(array)(array); // 使用数组版本进行析构
deleteAutodecltype(pointer)(pointer);// 使用指针版本进行析构
结合模板具体化与enable_if也可以实现对一类数据的筛选。例如我们需要对数字类型进行单独处理。首先需要编写判断类型是否为数组类型的代码
templatetypename T constexpr bool is_num() { return false; }
template constexpr bool is_numint() { return true; }
template constexpr bool is_numfloat() { return true; }
template constexpr bool is_numdouble() { return true; }
...
注意这里的函数必须要声明为constexpr这样才能在enable_if中使用。补充好所有我们认为是数字的类型就完成了。使用模板类也是可以完成这个任务的
templatetypename T struct is_num {constexpr static bool value false;
};
template struct is_numint {constexpr static bool value true;
};
... // 补充其他的数字类型
使用静态常量来表示这个类型是否为数字类型。静态常量也可以使用标准库的类减少代码量如下
templatetypename T struct is_num : public false_type {};
template struct is_numint : public true_type{};
... // 补充其他的数字类型
改为继承的写法但原理上是一样的。
有了以上的判断就可以使用enable_if来分类处理我们的逻辑了
templatetypename T void func(typename std::enable_ifis_numT(),T::type t) {//...
}
templatetypename T void func(typename std::enable_if!is_numT(),T::type t) {//...
}
使用enable_if的过程中还需要特别注意避免出现重载歧义或者优先级问题导致编程失败。
最后再补充一点关于匹配过程的类型问题。还是上面判断是否是数字的例子看下面的代码
int i ;
int r i;
funcdecltyper(r); // 无法判断是数字类型
在我们调用funcdecltypei(i);时i的类型是const int而我们具体化是template constexpr bool is_numint() { return true; }他的模板类型是int这是两个不同的类型无法对应。因此判断此类型为非数字类型。
导致这个问题不止有const还有volatile和引用类型。如int、volatile int等。解决这个问题的方法有两个
在具体化中增加const int等类型但是枚举所有的类型非常繁杂且容易遗忘。在匹配之前对数据类型进行去修饰处理。
第二种方法c提供函数处理。std::remove_referenceT::type移除类型的引用std::remove_cvT::type移除类型的const volatile修饰。因此我们在调用前可以如此处理
templatetypename T
using remove_cvRef typename std::remove_cvtypename std::remove_referenceT::type::type;
int i ;
int r i;
funcremove_cvRefdecltyper(r); // 移除引用修饰转化为int类型
关于类型推断相关的问题这里不多展开但要特别注意由于类型修饰导致的匹配失败问题。
最后
文章真的长呀如果你能坚持看到这里说明你是一个非常坚持且对编程有强烈兴趣的人希望这篇文章让你在c模板的路上有所帮助。
那么接下来我们再来回顾一下这篇文章的内容。
我们先介绍了模板元编程要解决的场景与问题然后我们从一个具体的模板元编程例子展开一步步学习了模板元编程的整体内容接下来针对其核心模板函数重载匹配规则以及模板规则进一步了解最后再给出在使用方面的一些经验供参考
模板元编程他要解决的最核心的问题就是对模板类型的判断与选择。而其所依赖的最核心的内容是模板函数重载匹配规则以及SFINAE法则他是我们模板元编程得以实现的基础。需要注意整个元编程发生在编译期任何的函数调用都无法通过编译。其次需要类型的推断导致的匹配错误问题而且此错误比较隐蔽难以发现。
最后模板元编程十分强大但涉及的相关内容多容易出错。只有当我们十分确定要使用模板元编程解决的问题再去使用他。切不可为了使用而使用成为自己炫技的工具这会给代码留下很多的隐患。
参考
C之std::declval-CSDN博客
C之std::enable_if-CSDN博客