当前位置:   article > 正文

深入解析C++的type_traits_条件类型 c++ typetraits

条件类型 c++ typetraits

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;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

基础类型判断

有了基础常量,我们可以先做一些简单的类型判断,比如说判断这个类型是不是void。这里的思路是,针对于所有类型的模板,继承自false_type,而针对于void类型,我们给予一个模板特例,让他继承自true_type。这样一来,只有当类型是void的时候才会推导true,其他的就会推导false。请看例程:

template <typename>
struct is_void : false_type {};

template <>
struct is_void<void> : true_type {};
  • 1
  • 2
  • 3
  • 4
  • 5

这里我们可以做一些简单的测试,来判断函数的返回值是否为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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

有了判断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 {};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

整型判断相对复杂一点,需要对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;
}
  • 1
  • 2
  • 3
  • 4
  • 5

但是照理来说,const类型以及引用类型不应该影响他浮点数的本质,当然,我们也可以针对所有的const以及引用情况都编写模板特例,但这样太麻烦了,如果有办法可以去掉const以及引用这些符号,然后再去判断的话,就会减少我们很多工作量。与此同时,这样的类型处理在实际编程时也是很有用的。
那么,如何去掉const?请看代码:

template <typename T>
struct remove_const {
    using type = T;
};

template <typename T>
struct remove_const<const T> {
    using type = T;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

同样的思路,当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;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

因此,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> {};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

类型选择

我们搞这样一系列的类型封装,最主要的原因是为了在编译器进行逻辑判断。因此,必然要进行一个选择逻辑,也就是当条件成立时,选择某一个类型,不成立时选择另一个类型。这个功能非常好实现,请看代码:

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;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

当第一个参数为true时,type就与T1相同,否则就与T2相同。

判断是否相同

我们有时候还需要判断两个类型是否相同,这部分也很好实现,请看代码:

template <typename, typename>
struct is_same : false_type {};

template <typename T>
struct is_same<T, T> : true_type {};
  • 1
  • 2
  • 3
  • 4
  • 5

tips

其实按照这些逻辑,我们几乎可以写出type_traits中的所有功能了。STL中还实现了合取、析取、取反等操作,只是将逻辑判断转为了模板形式,这些用起来更方便,但不是必须的。大家感兴趣可以阅读这部分源码。

实现is_base_of

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

__is_base_of关键字就可以完成这样的工作,所以我们封装它为模板即可:

template <typename B, typename D>
struct is_base_of : conditional<__is_base_of(B, D), true_type, false_type> {};
  • 1
  • 2

但除了这种直接使用编译器提供的关键字外,这个功能还有一种其他的实现方法。
如何判断一个类是否为一个类的父类呢?其实就看指针能否转换(多态)即可。请看代码:

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))) {};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

如果D是B的子类,那么就会调用第一个函数,从而推断出返回值是true_type,否则调用第二个函数,推断出返回值是false_type。
不过这样做还必须加一个判断,就是B和D必须都是类才行,而且需要去掉const等因素,详细代码读者可以自行尝试,不再赘述。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/AllinToyou/article/detail/310227
推荐阅读
相关标签
  

闽ICP备14008679号