模板编程是一种静态多态,它发生在编译期 ,另外函数重载也是一种静态多态,因此我们也称函数重载为
函数多态。
这里需要说明的一点,任何一种技术,我们都不能滥用,简单的说不能为了用而用,我们需要清楚,什么场景下,可以使用模板,
什么场景下,可以考虑使用动态多态(虚函数多态)。
例如,我以前一个同事在一个业务类中写了一个这样的模板函数:
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
评论(0)