C++的type_traits是一套纯粹编译期的逻辑,可以进行一些类型判断、分支选择等,主要用于模板编程。使用type_traits并不难,但是我们希望能够更加深入了解其实现方式,与此同时,可以更进一步体验C++的模板编程。
本篇文章旨在引导大家自行实现type_traits的基础代码。
模板编程不像常规的代码,可以有if-else这些流控制语句,我们需要充分利用模板、模板特例、类型转换等特性来实现编译期的一系列判断和类型转换。
第一步,我们需要定义true和false两个常量,所有的type_traits都基于此。我们的目的就是要用一个模板类型来表示是非,其中的value正好是这两个值。之后我们更高级的判断类型都是继承自这两个类型的其中一个,通过这种方式获取value值就可以获取true和false了。
如果听这个解释有点晕的话,不要紧,我们直接来看代码。这里需要注意的是,既然type_traits都是编译期行为,因此其成员只能是静态不可变成员(编译期就可以确定的成员)。
struct true_type {
static constexpr bool value = true;
};
struct false_type {
static constexpr bool value = false;
};
有了基础常量,我们可以先做一些简单的类型判断,比如说判断这个类型是不是void。这里的思路是,针对于所有类型的模板,继承自false_type,而针对于void类型,我们给予一个模板特例,让他继承自true_type。这样一来,只有当类型是void的时候才会推导true,其他的就会推导false。请看例程:
template <typename>
struct is_void : false_type {};
template <>
struct is_void<void> : true_type {};
这里我们可以做一些简单的测试,来判断函数的返回值是否为void:
void test1();
int test2();
int main(int argc, const char *argv[]) {
std::cout << is_void<decltype(test1())>::value << std::endl; // 1
std::cout << is_void<decltype(test2())>::value << std::endl; // 0
return 0;
}
有了判断void的思路基础,不难写出判断其他类型的,比如说判断是否为浮点数,那么只需要对float,double,long double进行特殊处理即可,请看代码:
template <typename>
struct is_floating_point : false_type {};
template <>
struct is_floating_point<float> : true_type {};
template <>
struct is_floating_point<double> : true_type {};
template <>
struct is_floating_point<long double> : true_type {};
整型判断相对复杂一点,需要对char,signed char,unsigned char,short,unsigned short,int,unsigned,long,unsigned long,long long,unsigned long long都进行特例编写,方法相同,不再赘述。
在上一节编写is_floating_point的时候可能会发现这样的问题:
int main(int argc, const char *argv[]) {
std::cout << is_floating_point<const double>::value << std::endl; // 0
std::cout << is_floating_point<double &>::value << std::endl; // 0
return 0;
}
但是照理来说,const类型以及引用类型不应该影响他浮点数的本质,当然,我们也可以针对所有的const以及引用情况都编写模板特例,但这样太麻烦了,如果有办法可以去掉const以及引用这些符号,然后再去判断的话,就会减少我们很多工作量。与此同时,这样的类型处理在实际编程时也是很有用的。
那么,如何去掉const?请看代码:
template <typename T>
struct remove_const {
using type = T;
};
template <typename T>
struct remove_const<const T> {
using type = T;
};
同样的思路,当T是const类型时,我们变换成const T,然后只取出T,其他类型时直接透传T。
同理,用这种方法也可以去除引用:
template <typename T>
struct remove_reference {
using type = T;
};
template <typename T>
struct remove_reference<T &> {
using type = T;
};
template <typename T>
struct remove_reference<T &&> {
using type = T;
};
因此,is_floating_point就可以改写成这样:
// 基础判断降级为helper
template <typename>
struct is_floating_point_helper : false_type {};
template <>
struct is_floating_point_helper<float> : true_type {};
template <>
struct is_floating_point_helper<double> : true_type {};
template <>
struct is_floating_point_helper<long double> : true_type {};
// remove_reference和remove_const的声明
template <typename>
struct remove_const;
template <typename>
struct remove_reference;
// 实际的is_floating_point
template <typename T>
struct is_floating_point : is_floating_point_helper<typename remove_const<typename remove_reference<T>::type>::type> {};
我们搞这样一系列的类型封装,最主要的原因是为了在编译器进行逻辑判断。因此,必然要进行一个选择逻辑,也就是当条件成立时,选择某一个类型,不成立时选择另一个类型。这个功能非常好实现,请看代码:
template <bool judge, typename T1, typename T2>
struct conditional {
using type = T1;
};
template <typename T1, typename T2>
struct conditional<false, T1, T2> {
using type = T2;
};
当第一个参数为true时,type就与T1相同,否则就与T2相同。
我们有时候还需要判断两个类型是否相同,这部分也很好实现,请看代码:
template <typename, typename>
struct is_same : false_type {};
template <typename T>
struct is_same<T, T> : true_type {};
其实按照这些逻辑,我们几乎可以写出type_traits中的所有功能了。STL中还实现了合取、析取、取反等操作,只是将逻辑判断转为了模板形式,这些用起来更方便,但不是必须的。大家感兴趣可以阅读这部分源码。
is_base_of用于判断两个类型是否是继承关系,在C++中已经存在了对应的关键字用于判断:
struct B {};
struct D : B {};
struct A {};
int main(int argc, const char *argv[]) {
std::cout << __is_base_of(B, D) << std::endl; // 1
std::cout << __is_base_of(B, A) << std::endl; // 0
return 0;
}
__is_base_of关键字就可以完成这样的工作,所以我们封装它为模板即可:
template <typename B, typename D>
struct is_base_of : conditional<__is_base_of(B, D), true_type, false_type> {};
但除了这种直接使用编译器提供的关键字外,这个功能还有一种其他的实现方法。
如何判断一个类是否为一个类的父类呢?其实就看指针能否转换(多态)即可。请看代码:
template <typename B, typename D>
true_type test_is_base(B *);
template <typename B, typename D>
false_type test_is_base(void *);
template <typename B, typename D>
struct is_base_of : decltype(test_is_base<B, D>(static_cast<D *>(nullptr))) {};
如果D是B的子类,那么就会调用第一个函数,从而推断出返回值是true_type,否则调用第二个函数,推断出返回值是false_type。
不过这样做还必须加一个判断,就是B和D必须都是类才行,而且需要去掉const等因素,详细代码读者可以自行尝试,不再赘述。