中心思想
C++中可以在编译期对类型进行计算,借助模板参数进行匹配尝试。准确的说是通过模板类的特化和函数的重载来实现,及SFINAE和 函数的overload resolution。
提前准备知识点
a、C++中使用 ... 表示包罗万象的工具,作为模板的模板参数和函数的参数表示任意个数、任意类型的参数。
b、对于任意类型T,想要创建一个和T 相关的缺省参数,则可以这样 T* t = nullptr; (空指针nullptr 真是个极好的抽象!)。
c、编译期的常量有 enum 枚举类型和 static const 两种。
d、对表达式类型进行计算的 sizeof , decltype 都不会对表达式求值。
一、判断一个类型是否为指针。
1、通过类模板特化来实现:
template <typename T>
struct isPointer
{
static const bool value = false;
};
template <typename T>
struct isPointer<T*>
{
static const bool value = true;
};
使用时,isPointer<Type>::value 即可。
2、通过函数重载来实现。
总体框架如下,通过在类内部定义static 函数决议来选中指针版本,通过函数的返回值大小判断最终决议选中的版本。
template <typename T>
struct isPointer
{
... ...
}
由于函数模板不支持偏特化, 不能这样写:
template<typename U>
static char func<U*>(U*); //函数模板不支持偏特化
那就只能用函数模板的重载
template<typename U>
static char func(U*);
这样写,模板的参数类型是T,函数的参数类型是U*,那么 isPointer<T>::func() 的参数类型是T*,另外又添加了一个*, 而我们要判断的是T是否为指针,显然不能这样。而T如果为指针类型,刚好能和U*想匹配,所以考虑定义一个T类型的对象。
template <typename T>
struct isPointer
{
static T obj; //必须声明为static的 因为在static 的成员函数中需要访问(static成员函数没有this 指针)
static int func(...); //version 1
template<typename U>
static char func(U*); //version 2
static const bool value = (
sizeof(func(obj)) == sizeof(char) //当T为类类型时,这里甚至都不需要T含有默认的构造函数,因为我们并没有定义这样一个对象出来!!!
);
};
使用时,isPointer<Type>::value 即可。值得注意的地方:1、函数func 并没有提供定义,因为sizeof() 只会对表达式求类型而不会求值,所以不会发生函数的调用过程,所以不需要定义。2、函数func 被声明为static,这是因为在sizeof 的表达式中,不需要一个isPointer 的对象。3、两个重载的版本的返回值的类型只要字节数不一样即可。4、版本一是一定需要的,使版本二不能匹配的时候能有一个可以匹配上,否则会发生编译错误。
三天之后的回顾:实际上这个实现方法有bug,原因在于C++中存在表达式的类型退化现象。
上图是四中基本的表达式的类型退化,可以组合。上边函数重载方法中表达式obj 的类型可能会发生退化,而不再是类型T,比如T的类型为 int [6], 用上边的方法 isPointer<int [6]>::value 得到的却是true。会退化的表达式都是不能被赋值的(引用的不能被赋值是指引用一旦绑定某个对象后,不能解绑定)。
二、判断一个类型是否为类类型还是普通类型
1、、使用类模板特化
预备知识
a、对于类模板,用主模板去推导和特化模板去推导,得到的模板ID相同,则使用特化版本;否则如果主模板能够匹配,则使用主模板。
b、对于类类型,总是可以定义指向任意类型的成员函数指针,尽管并不存在该类型的成员。
template<class T>
struct help
{
using Type = T;
};
template<class T>
struct help<int T::*>
{
using Type = T; //当T是类类型时,使得内部类型为T 这样推导时isClass的特化版本能和主板版本相同
};
template<class T , class = T> //这里提供一个默认模板参数,使得实例化时只用提供一个
struct isClass
{
static const bool value=false;
};
template<class T>
struct isClass<T, typename help<int T::*>::Type > //第一个模板参数要用到T 否则无法推导
{
static const bool value=true;
};
使用时,只需 isClass<Type> 即可。
这里构造了一个辅助类,当T是类类型时,特换版本也可以推导为 isClass<T, T>,和主模板的推导结果相同,于是选择特化版本。注意特化版本的第一个T是必须的,否则会发生无法推导的错误(见这里 的 和这里 的说明)。 另外,特化版本的第二个模板参数的类型可以通过第一个模板参数推导出来,所以实例化时只需提供一个。
2、使用函数重载
template<class T>
struct isClass
{
template<typename U>
static int func(...);
template<typename U>
static char func(int U::*);
static const bool value = ( sizeof(func<T>(nullptr)) == sizeof(char) );
};
提供一个约束版本和一个任意参数版本,在约束版本不能替换成功时,还有任意参数版本可以替换成功。所有可能的函数模板构成一个候选集,从候选集中挑选一个,决胜者得到最终的匹配。
只有在编译器选中主函数模板时,全特化版本才可能被使用。任意参数版本,无论是模板版本还是普通函数版本,都只有当所有候选版本都不能替换成功时,才会匹配。判断是否为指针中使用的是普通的任意参数函数,此处使用的是模板版本,原因在于func<T>() 说明此时只能是模板版本。
另外关于函数模板的主模板和全特化版本之间的决议问题(函数模板只能全特化,不能偏特化(特化在template 后一个<>, 模板之后一个<>),偏特化可以用重载等效,当然全特化也可以用重载等效)。重载决议的顺序:普通函数 > 主模板(未特化的模板) > 全特化版本。(C++编程剖析问题、方案和设计准则 P43)
template<typename T>
void func(T t)
{
std::cout << "version 1";
}
template<>
void func(int& t)
{
std::cout << "version 2";
}
int main(int argc, char *argv[])
{
int i(2);
func(i); //version1
std::cin.get();
return 0;
} //第一个版本将被选中
用主模板去推导,得到的将是 func(int arg); 与全特化版本不同,所以选择主模板。kiss原则:都能匹配,简单的优先。
实际上函数模板的全特化是种非常不好的设计, 毕竟全特化之后,相当于普通函数了,但是重载决议时,容易出现问题。所以将全特化改为普通的函数,优先级还最高,和期望预期。总是和期望预期。
template<typename T>
void func(T t)
{
std::cout << "version 1";
}
template<>
void func(int const* t)
{
std::cout << "version 2";
}
int main(int argc, char *argv[])
{
int i(2);
func(&i); //version 1
std::cin.get();
return 0;
}
同样的,因为全特化中,形参t 是某种类型的指针,非const,用主模板去推导,推导的结果是 func(int* t), 和全特化版本不同,所以选择主模板版本。
template<typename T>
void func(T t)
{
std::cout << "version 1";
}
template<>
void func(int* const t)
{
std::cout << "version 2";
}
int main(int argc, char *argv[])
{
int i(2);
func(&i); //version 2
std::cin.get();
return 0;
}
注意,此时选择的将是特化版本。主模板推导出的结果为 func(int* t), 全特化版本为 func(int* const), 主模板加上const 和全特化相同,选择全特化版本。
template<typename T>
void func(T const t)
{
std::cout << "version 1";
}
template<>
void func(int* t)
{
std::cout << "version 2";
}
int main(int argc, char *argv[])
{
int i(2);
func(&i);
std::cin.get(); //versiont 2
return 0;
}
此时主模板的推导结果去掉 const 和全特化版本相同,所以选择全特化版本。
C++有类型和表达式两类数据。上边两个例子都是直接对类型操作,如果是表达式(表达式有类型和值双重属性,如果值是左值,还对应着一个地址。),判断表达式的类型,则只能用重载的方法。比如判断一个表达式的类型是否为指针:
int func(...); //version 1
template<typename U>
char func(U*); //version 2
#define isExprPointer(expr) (sizeof(func(expr)) == sizeof(char))
//测试
int main()
{
const int* const p = nullptr;
std::cout << isExprPointer(p);
std::cin.get();
return 0;
}
或者有了对类型的操作,使用 decltype(expr) 获得表达式 expr 的类型,然后对decltype(expr) 直接进行处理,也不失为一种方法。另外,注意除了Type Failure,还有 Expression Failure. 于是,SFINAE也就有了 Type 和 Expression两种。
最后,函数的重载决议是很难的东西,非常复杂,唔,为了减少烦恼,使用enable_if 增加一个缺省的参数是极好的方法。
判断类是否有某方法的代码:
template<typename T, typename RESULT, typename ... Arg>
class HasPolicy
{ //模板的非类型参数只能是广义的"整形"
template <typename U, RESULT (U::*)(Arg...)> struct Check; //注意这里 RESULT (U::*)(Arg...) 是表达式,而非类型,前边没有typename
template <typename U> static char func(Check<U, &U::policy> *);
template <typename U> static int func(...);
public:
typedef HasPolicy type;
enum { value = sizeof(func<T>(0)) == sizeof(char) };
};
class Test
{
public:
int policy(int, double)
{
return 0;
}
};
可以用宏将名称屏蔽起来,这样就可以检测类是否有函数签名为 int policy(int, double) 的方法了。
进一步将返回值和参数抽象起来,直接判断是否有名字为 XXX 的成员方法。
#define hasMethod(Method)\
template <typename T>\
class has_##Method\
{\
typedef char one;\
typedef long two;\
template <typename C> static one test( decltype(&C::Method) ) ;\ //这里使用decltype 获取类型,而不再传入一个模板的非类型参数
template <typename C> static two test(...);\
public:\
enum { value = sizeof(test<T>(nullptr)) == sizeof(char) };\
};
hasMethod(helloworld);
int main()
{
std::cout << HasPolicy<Test, int, int, double>::value << std::endl;
std::cout << has_helloworld<Hello>::value << std::endl;
std::cout << has_helloworld<Generic>::value << std::endl;
return 0;
}