C++超详细分析type_traits

目录
  • 定义基础常量
  • 基础类型判断
  • 类型处理
  • 类型选择
  • 判断是否相同
  • tips
  • 实现is_base_of

本篇文章旨在引导大家自行实现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 {};

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;
}

__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等因素,详细代码读者可以自行尝试,不再赘述。

到此这篇关于C++超详细分析type_traits的文章就介绍到这了,更多相关C++ type_traits内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 详解C++中typedef 和 #define 的区别

    1.执行上不同 关键字 typedef 在编译阶段有效,由于是在编译阶段,因此 typedef 有类型检查的功能. #define 则是宏定义,发生在预处理阶段,也就是编译之前,它只进行简单而机械的字符串替换,而不进行任何检查. 例如:typedef 会做相应的类型检查 typedef unsigned int UINT; void func() { UINT value = "abc"; // error C2440: 'initializing' : cannot convert

  • 深入浅析C++ traits技术

    前言 traits,又被叫做特性萃取技术,说得简单点就是提取"被传进的对象"对应的返回类型,让同一个接口实现对应的功能.因为STL的算法和容器是分离的,两者通过迭代器链接.算法的实现并不知道自己被传进来什么.萃取器相当于在接口和实现之间加一层封装,来隐藏一些细节并协助调用合适的方法,这需要一些技巧(例如,偏特化).最后附带一个小小的例子,应该能更好地理解 特性萃取. 下面大部分来源于<STL源码剖析>,看原书能了解更多细节. Traits编程技法 让我们一点点抛出问题,然后

  • 十分钟学会C++ Traits

    最近和一个朋友闲聊的时候他对我说一个人对C++的理解很多种境界,朋友不是个喜欢吹牛的人,于是听他细说,觉得很是有道理. 想写一篇C++ traits方面的文章已经有一段时间了,但是说实话traits这项技术确实有些晦涩,很担心写完了达不到期望的效果,于是每每试图以简炼的文字表达,慢慢的就等到了今天. 先说说我为什么专门对这项技术写一篇文章吧.记得当时在看STL/boost代码的时候经常遇到traits,当时惊叹于代码原来可以这样写,但是最初根本是看不懂的,查了一些资料才彻底理解了traits存在

  • C++中typeid实现原理详解

    最近看了boost::any类源码,其实现主要依赖typeid操作符.很好奇这样实现的时间和空间开销有多大,决定探一下究竟. VS2008附带的type_info类只有头文件,没有源文件,声明如下: class type_info { public: virtual ~type_info(); _CRTIMP_PURE bool __CLR_OR_THIS_CALL operator==(const type_info& rhs) const; _CRTIMP_PURE bool __CLR_O

  • c++ typeid关键字的使用

    typeid关键字 注意:typeid是操作符,不是函数.这点与sizeof类似) 运行时获知变量类型名称,可以使用 typeid(变量).name() 需要注意不是所有编译器都输出"int"."float"等之类的名称,对于这类的编译器可以这样使用 int ia = 3; if(typeid(ia) == typeid(int)) { cout <<"int" <<endl; } RTTI(Run-Time Type I

  • C++ typeid 和虚函数详解

    目录 typeid 和虚函数 总结 typeid 和虚函数 前面咱们讲到 typeid 的操作返回值是 type_info 对象的引用,然后输出返回值的地址是相同的,测试代码如下: #include <iostream> #include <functional> using namespace std; class Base{ public: virtual void test(){ cout << "Base::test" << en

  • 从c++标准库指针萃取器谈一下traits技法(推荐)

    本篇文章基于gcc中标准库源码剖析一下标准库中的模板类pointer_traits,并且以此为例理解一下traits技法. 说明一下,我用的是gcc7.1.0编译器,标准库源代码也是这个版本的. 还是先看一下思维导图,如下: 1. 指针萃取器pointer_traits说明 首先说明一下哈,官方并没有指针萃取器这个名称,其实pointer_traits是类模板,它是c++11以后引入的,可以通过传入的重绑定模板类型得到相应的指针类型,比较官方的描述是:pointer_traits 类模板提供标准

  • C++超详细分析type_traits

    目录 定义基础常量 基础类型判断 类型处理 类型选择 判断是否相同 tips 实现is_base_of 本篇文章旨在引导大家自行实现type_traits的基础代码. 模板编程不像常规的代码,可以有if-else这些流控制语句,我们需要充分利用模板.模板特例.类型转换等特性来实现编译期的一系列判断和类型转换. 定义基础常量 第一步,我们需要定义true和false两个常量,所有的type_traits都基于此.我们的目的就是要用一个模板类型来表示是非,其中的value正好是这两个值.之后我们更高

  • Java超详细分析泛型与通配符

    目录 1.泛型 1.1泛型的用法 1.1.1泛型的概念 1.1.2泛型类 1.1.3类型推导 1.2裸类型 1.3擦除机制 1.3.1关于泛型数组 1.3.2泛型的编译与擦除 1.4泛型的上界 1.4.1泛型的上界 1.4.2特殊的泛型上界 1.4.3泛型方法 1.4.4类型推导 2.通配符 2.1通配符的概念 2.2通配符的上界 2.3通配符的下界 题外话: 泛型与通配符是Java语法中比较难懂的两个语法,学习泛型和通配符的主要目的是能够看懂源码,实际使用的不多. 1.泛型 1.1泛型的用法

  • 非常适合新手学生的Java线程池超详细分析

    目录 线程池的好处 创建线程池的五种方式 缓存线程池CachedThreadPool 固定容量线程池FixedThreadPool 单个线程池SingleThreadExecutor 定时任务线程池ScheduledThreadPool ThreadPoolExecutor创建线程池(十分推荐) ThreadPoolExecutor的七个参数详解 workQueue handler 如何触发拒绝策略和线程池扩容? 线程池的好处 可以实现线程的复用,避免重新创建线程和销毁线程.创建线程和销毁线程对

  • C++超详细分析红黑树

    目录 红黑树 红黑树的概念 红黑树的性质 红黑树结点的定义 红黑树的插入操作 情况一 情况二 情况三 红黑树的验证 用红黑树封装map.set 红黑树的迭代器 封装map 封装set 红黑树 红黑树的概念 红黑树的概念 红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black. 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的. 红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是

  • C++ 超详细分析数据结构中的时间复杂度

    别别着急划走哈,如果你跟我一样是大学生,那么你发现了一个宝藏!我们往后看--> 要想了解时间复杂度和空间复杂度,我们得知道什么是时间复杂度和空间复杂度! 有的人看到这就明白了,而有的人却去追求它的内涵: 见名知意嘛,时间复杂度不就是表示一个算法运行完所需要的时间?这还用问?错错错! 我来举一个很简单的例子:你家隔壁老王买了一台 i9 12900k 和 RTX3080Ti 整个64GB的内存,你眼瞅着你 4G的内存,洋垃圾的处理器,打开个PS都要冒烟的那种,来来来,你跟我说说能比吗? 所以简单来说

  • Java数据结构超详细分析二叉搜索树

    目录 1.搜索树的概念 2.二叉搜索树的简单实现 2.1查找 2.2插入 2.3删除 2.4修改 3.二叉搜索树的性能 1.搜索树的概念 二叉搜索树是一种特殊的二叉树,又称二叉查找树,二叉排序树,它有几个特点: 如果左子树存在,则左子树每个结点的值均小于根结点的值. 如果右子树存在,则右子树每个结点的值均大于根结点的值. 中序遍历二叉搜索树,得到的序列是依次递增的. 二叉搜索树的左右子树均为二叉搜索树. 二叉搜索树的结点的值不能发生重复. 2.二叉搜索树的简单实现 我们来简单实现以下搜索树,就不

  • Java 栈与队列超详细分析讲解

    目录 一.栈(Stack) 1.什么是栈? 2.栈的常见方法 3.自己实现一个栈(底层用一个数组实现) 二.队列(Queue) 1.什么是队列? 2.队列的常见方法 3.队列的实现(单链表实现) 4.循环队列 一.栈(Stack) 1.什么是栈? 栈其实就是一种数据结构 - 先进后出(先入栈的数据后出来,最先入栈的数据会被压入栈底) 什么是java虚拟机栈? java虚拟机栈只是JVM当中的一块内存,该内存一般用来存放 例如:局部变量当调用函数时,我们会为函数开辟一块内存,叫做 栈帧,在 jav

  • C++ 超详细分析多态的原理与实现

    目录 多态的定义及实现 多态的构成条件 虚函数重写 C++11的override和final 抽象类 多态的原理 虚函数表 动态绑定与静态绑定 单继承和多继承关系的虚函数表 单继承中的虚函数表 多继承中的虚函数表 常见问题 多态的定义及实现 多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态. 比如买票这个行为,当普通人买票时,是全价买票:学生买票时,是半价买票:军人买票时是优先买票. 多态的构成条件 多态是在不同继承关系的类对象,去调用同一函数

  • C++ 超详细分析数据结构中的时间复杂度

    目录 什么是时间复杂度和空间复杂度 如何计算时间复杂度和空间复杂度 如何计算时间复杂度和空间复杂度 别别着急划走哈,如果你跟我一样是大学生,那么你发现了一个宝藏!我们往后看--> 什么是时间复杂度和空间复杂度 要想了解时间复杂度和空间复杂度,我们得知道什么是时间复杂度和空间复杂度! 有的人看到这就明白了,而有的人却去追求它的内涵: 见名知意嘛,时间复杂度不就是表示一个算法运行完所需要的时间?这还用问?错错错! 我来举一个很简单的例子:你家隔壁老王买了一台 i9 12900k 和 RTX3080T

  • 超详细分析C语言动态内存管理问题

    目录 一.为什么存在动态内存的分配 二.动态内存函数的介绍 2.1 malloc和free 2.2 calloc 2.3 realloc 三.常见的动态内存错误 3.1 对NULL指针的解引用操作 3.2 对动态开辟空间的越界访问 3.3 对非动态开辟内存使用free释放 3.4 对同一块动态内存多次释放 3.5 动态开辟内存忘记释放(内存泄漏) 四.几个经典的笔试题 五.C/C++程序的内存开辟 六.柔性数组 6.1 柔性数组的特点 6.2 柔性数组的使用 6.3 柔性数组的优势 上期结束了[

随机推荐