问题
最近要写一个模板函数,要求是必须被指定的类型才能执行,未被指定的类型要求报错。
注册器模板类
其实所谓接受指定类型作为参数就是对类型进行注册,凡是注册过的类型才能传入模板,非注册类型应当匹配失败,所以直接使用一个模板类的特化来实现注册。
template <typename T>
struct Register{};
template <>
struct Register<myint> {
typedef myint Type;
constexpr static const char* TypeName = "myint";
};
template <>
struct Register<mylong> {
typedef mylong Type;
constexpr static const char* TypeName = "mylong";
};
template <>
struct Register<float> {
typedef float Type;
constexpr static const char* TypeName = "float";
};
这里普通类是空的没有任何成员,而凡是特化的类都定义了对应的特化类型成员。下面的const char*只是为了检查用的,可有可无。
这样我们相当于注册了三个类型进入注册器,然后就可以利用这个注册器来完成我们需要的模板函数。
模板函数实现
template <typename T, typename = typename Register<T>::Type>
void print(const T& x) {
cout<<Register<T>::TypeName<<'\t'<<x<<'\n';
}
这个模板函数有两个模板参数,分别是T和Register::Type类型,如果Register::Type存在则模板匹配成功,如果不存在则模板匹配失败,由于我们的注册器默认是空,不存在Type类型,所以未被特化的类型必然匹配失败,从而实现了只允许特定类型传入函数。
测试一下
int main() {
print(-1);
print((long long int)(2147483650));
print(12.11f);
return 0;
}
可以正常编译允许,运行结果为
myint -1
mylong 2147483650
float 12.11
而如果我们把12.11f中的f去掉编译就会保存,提示没有print(const double&)的匹配函数。可以看到通过这种方法可以实现类型注册机制,每当我们需要添加一个新类型时只需要添加一个Register的特化即可,这种方法还有一个好处是如果我们的函数(这里的print)如果还要根据传入类型不同做区分操作,我们只需要在特化类中添加一个static的函数即可实现不同传入类型的区别操作。有点类似我在Register特化时加入的const char*变量实现了不同类型返回各自类型的类型名。
对vector和initializer_list特殊操作
如果当我注册了类型T之后,我希望我的函数还可以接收vector和{T(t1),T(t2)}这种初始化序列并且执行对应操作,那么我们只需要添加如下两个函数即可
template <typename T, typename = typename Register<T>::Type>
void print(const vector<T>& x) {
cout<<"v_"<<Register<T>::TypeName<<'\t';
for(const auto& i:x)
cout<<i<<'\t';
cout<<endl;
}
template <typename T, typename = typename Register<T>::Type>
void print(const initializer_list<T>& x) {
cout<<"v_"<<Register<T>::TypeName<<'\t';
for(const auto& i:u)
cout<<i<<'\t';
cout<<endl;
}
测试一下
int main() {
print(vector<float>({12.11,9.9}));
print({1,2,3,4});
return 0;
}
运行结果为
v_float 12.11 9.9
v_myint 1 2 3 4
可以看到同样可以得到正确的结果。
邪道
上面是正常的一种实现方法,比较容易理解,而且可以根据注册类型不同实现函数细节处理的区别,就好像前面cout的时候可以输出不同的参数类型名称。如果仅仅只想要注册类型限制函数传入的参数类型,那么还有下面这种邪道做法,可以做很小的修改实现新类型注册,不过由于模板我本来也只是刚刚开始学习,因此这个实现只是一个原型,仅仅实现了功能并不算特别好用。
template <typename... T>
struct Regist{};
template <typename U,typename... T>
struct isRegister {};
template <typename U,typename t,typename... T>
struct isRegister<U,t,T...>{
enum {value = is_same<t,U>::value|| isRegister<U,T...>::value};
};
template <typename U>
struct isRegister<U> {enum {value=0};};
这里定义了两个类Regist和isRegister其中Regist没有意义,仅仅用来传递模板参数,isRegister通过递归的方法遍历变长模板参数,找到变长模板参数中是否有可以匹配到的类型,有则返回1,没有则返回0。
然后就是定义一个邪道模板函数了。
template <typename T,typename... U>
bool xiedao(const T& a,const Regist<U...>& b) {
return isRegister<T,U...>::value;
};
这个函数接收两个参数,一个是我们希望判断是否被注册的类型的变量,另一个是我们前面定义的用来传递模板参数的Regist类型的实例,由于Regist是空的,其实例没有任何意义,有意义的仅是其实例化时其类型的模板参数。typedef可以完成一个注册表的定义,然后利用这个注册表和xiedao函数就可以完成类型注册匹配了。具体使用方法如下
typedef Regist<int,float,mylong> Reg;
int main() {
typedef Regist<int,float,mylong> Reg;
cout<<"xie dao cheng gong le ma?"<<xiedao(int(),Reg())<<'\n';
cout<<"xie dao cheng gong le ma?"<<xiedao(float(),Reg())<<'\n';
cout<<"xie dao cheng gong le ma?"<<xiedao(mylong(),Reg())<<'\n';
cout<<"xie dao cheng gong le ma?"<<xiedao(double(),Reg())<<'\n';
return 0;
}
运行结果如下
xie dao cheng gong le ma?1
xie dao cheng gong le ma?1
xie dao cheng gong le ma?1
xie dao cheng gong le ma?0
可以看到我们注册了int,float,mylong三个类型,其中int、float和mylong都成功匹配了,double匹配失败,完成了我们的功能。这样我们需要注册什么类型只需要在typedef的注册表中添加进去即可。
后来通过尝试对上面的实现做了些许改进,现在用起来更加自然了。
template <typename... T>
struct Regist{};
template <typename U,typename T>
struct isRegister {};
template <typename U,typename t,typename... T>
struct isRegister<U,Regist<t,T...>>{
enum {value = is_same<t,U>::value|| isRegister<U,Regist<T...>>::value};
};
template <typename U>
struct isRegister<U,Regist<>> {enum {value=0};};
这里就不再需要构造临时的对象来传递模板参数了,直接把Regist作为模板参数传入isRegister即可。然后同样递归匹配注册类型。这时的使用就显得更加自然。
int main() {
typedef Regist<int,float,mylong> Reg;
cout<<isRegister<int,Reg>::value<<'\n';
cout<<isRegister<float,Reg>::value<<'\n';
cout<<isRegister<mylong,Reg>::value<<'\n';
cout<<isRegister<double,Reg>::value<<'\n';
return 0;
}
运行结果如下
1
1
1
0
这样就可以配合enable_if实现编译期的匹配判定。