还搞不懂STL的type_traits?从源码来带你一起分析

Posted 凌桓丶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了还搞不懂STL的type_traits?从源码来带你一起分析相关的知识,希望对你有一定的参考价值。

文章目录


什么是类型萃取?

type_traits被称为类型萃取,主要用于在编译期计算、查询、判断、转换和选择,增强了泛型编程的能力,也增强了程序的弹性,使得我们在编译期就能做到优化改进甚至排错,能进一步提高代码质量。

cplusplus-type_traits
在C++中类型萃取主要包含三大部分

  • 辅助类(Helper classes)

    • 用于帮助创建编译器常量的标准类。
  • 类型萃取(Type traits)

    • 在编译期以常量的形式获取类的特征。
  • 类型转换(Type transformations)

    • 为某个类型增加/删除属性。例如增加/删除const、volatile等。


源码剖析

以下代码全部来自C++标准库的type_traitsxtr1common,SGI-STL的type_traits.h文件中,为了方便阅读与理解,在一些重要的地方我会加上注释。

SGI-STL G2.9版本类型萃取

考虑到理解难度,我们先不讲标准库的类型萃取,先看看G2.9的STL中单独封装的类型萃取。

在SGI-STL的类型萃取中,每个类型其主要关注六个参数

  • this_dummy_member_must_be_first:这个虚函数成员必须为第一个参数
  • has_trivial_default_constructor:默认的构造函数是否不重要。
  • has_trivial_copy_constructor:拷贝构造函数是否不重要。
  • has_trivial_assignment_operator:赋值运算符是否不重要。
  • has_trivial_destructor:析构函数是否不重要。
  • is_POD_type:是否为基本类型。

为什么要选择这些参数呢?例如我们在使用一个容器时(例如vector<type>),如果其为自定义类型,我们需要在构造的时候为每一个成员调用一次构造函数,在析构的时候调用一次析构函数。但是对于内置类型来说,这些操作完全不必要,我们可以直接采用内存直接处理操作如malloc()memcpy()等方法来以最高效率执行。

如果我们准备对一个类型未知的数组进行拷贝操作时,如果能够提前知道它使用的是默认的拷贝构造函数,就可以直接使用memcpymemmove来快速进行拷贝,对于STL中这些大规模且操作频繁的容器,带来了巨大的性能提升。

下面结合源码,来看看它们是怎么实现的

struct __true_type 
;

struct __false_type 
;

//泛化模板
template <class type>
struct __type_traits  
   typedef __true_type     this_dummy_member_must_be_first;
   typedef __false_type    has_trivial_default_constructor;
   typedef __false_type    has_trivial_copy_constructor;
   typedef __false_type    has_trivial_assignment_operator;
   typedef __false_type    has_trivial_destructor;
   typedef __false_type    is_POD_type;
;

//特化,太多了这里只举几个例子
__STL_TEMPLATE_NULL struct __type_traits<char> 
   typedef __true_type    has_trivial_default_constructor;
   typedef __true_type    has_trivial_copy_constructor;
   typedef __true_type    has_trivial_assignment_operator;
   typedef __true_type    has_trivial_destructor;
   typedef __true_type    is_POD_type;
;

__STL_TEMPLATE_NULL struct __type_traits<int> 
   typedef __true_type    has_trivial_default_constructor;
   typedef __true_type    has_trivial_copy_constructor;
   typedef __true_type    has_trivial_assignment_operator;
   typedef __true_type    has_trivial_destructor;
   typedef __true_type    is_POD_type;
;

STL中的做法十分简单,其通过模板的特化来实现这个功能。对于泛化类型,它默认它的这些参数都是false的,而后再对所有的内置类型进行特化,将它们typedef为true,简单高效的完成了这个功能。

C++标准库类型萃取

C++标准库中的类型萃取功能更加强大,但是实现也更加的复杂

SFINAE机制与enable_if

SFINAE(Substitution Failure is Not An Error)是C++ 的一种语言属性,它的核心就是从一组重载函数中删除模板实例化无效的函数——在编译期编译时,会将函数模板的形参替换为实参,如果替换失败编译器不会当作是个错误,直到找到那个最合适的特化版本,如果所有的模板版本都替换失败那编译器就会报错。

这里用enable_if来举个例子

template <bool _Test, class _Ty = void>
struct enable_if ; 

template <class _Ty>
struct enable_if<true, _Ty> 
    using type = _Ty;
;

template <bool _Test, class _Ty = void>
using enable_if_t = typename enable_if<_Test, _Ty>::type;

从源码我们可以看到,只有当第一个模板参数为true的时候,由于偏特化,其能够匹配到第二个模板,此时的type才是有效的。而对于其他情况来说,编译器都会直接忽略,直到匹配所有模板都失败后,此时编译器才会进行报错。

通过SFINAE技术可以完成很多有趣的事,比如根据参数类型做不同的定制化操作。

conditional

为了能利用模板来实现如orand等逻辑判断,type_traits还实现了条件模板conditional

template <bool _Test, class _Ty1, class _Ty2>
struct conditional  // Choose _Ty1 if _Test is true, and _Ty2 otherwise
    using type = _Ty1;
;

template <class _Ty1, class _Ty2>
struct conditional<false, _Ty1, _Ty2> 
    using type = _Ty2;
;

template <bool _Test, class _Ty1, class _Ty2>
using conditional_t = typename conditional<_Test, _Ty1, _Ty2>::type;

其泛化时默认type为第二个参数,而对于第一个参数为false的特化版本,则会使用第二个参数。

核心结构:integral_constant 与bool_constant

type_traits最核心的结构就是integral_constantbool_constant,下面给出了它们的源代码

integral_constant是类型萃取最底层的结构,其借助模板使我们能够获取到KEY与VALUE的值与类型。同时,integral_constant重载了类型转换操作符与()操作符,使其能够像一个函数一样进行调用。

template <class _Ty, _Ty _Val>
struct integral_constant 
    static constexpr _Ty value = _Val;

    using value_type = _Ty;
    using type       = integral_constant;	

	//类型转换运算符重载,将对象转换为value_type时隐式调用
    constexpr operator value_type() const noexcept 
        return value;
    

	//()运算符重载,用于实现仿函数
    _NODISCARD constexpr value_type operator()() const noexcept 
        return value;
    
;

接着我们来分析bool_constant,其实它就是利用模板来对integral_constant进行了一层封装。它用来检查模板类型是否为某种类型,通过bool_constant我们可以获取编译期检查的bool值结果。

template <bool _Val>
using bool_constant = integral_constant<bool, _Val>;

using true_type  = bool_constant<true>;
using false_type = bool_constant<false>;


is_same

接下来我们看看is_same这个类,下面给出源码:

template <class _Ty1, class _Ty2>
struct is_same : bool_constant<is_same_v<_Ty1, _Ty2>> ;

如果只从调用关系看,我们很容易将其看成一个函数,但是实际上它其实继承于bool_constant,是一个仿函数对象。

它的主要作用是判断两个对象的类型是否相同,从源码我们可以看出,这里主要依赖了is_same_v这个模板变量。

//借助模板的匹配规则判断是否属于相同类型
template <class, class>
_INLINE_VAR constexpr bool is_same_v = false;

template <class _Ty>
_INLINE_VAR constexpr bool is_same_v<_Ty, _Ty> = true;

is_same_v借助模板的隐式匹配规则,来判断两个参数是否属于同一个类型,只有两个模板参数的类型都相同时,才为true。

类型转换:remove/add_xx()

在类型萃取的时候,为了排除不必要的干扰,我们通常会对数据的原类型进行一些处理,比如去掉const、volatile、右值引用等属性。

下面我们来看看比较常用的remove_constremove_volatile的源码:

//去除顶层const
template <class _Ty>
struct remove_const  
    using type = _Ty;
;

template <class _Ty>
struct remove_const<const _Ty> 
    using type = _Ty;
;

template <class _Ty>
using remove_const_t = typename remove_const<_Ty>::type;

// 去除顶层volatile
template <class _Ty>
struct remove_volatile  
    using type = _Ty;
;

template <class _Ty>
struct remove_volatile<volatile _Ty> 
    using type = _Ty;
;

template <class _Ty>
using remove_volatile_t = typename remove_volatile<_Ty>::type;

它们的实现思路相同,都是利用了模板的偏特化来完成类型的消除,当我们的参数匹配到特化版本时,就会通过using关键字将type指定为泛化版本,完成消除。

通常,这两个仿函数会搭配使用,因此标准库又封装它们再次成remove_cv

// STRUCT TEMPLATE remove_cv
template <class _Ty>
struct remove_cv  // remove top-level const and volatile qualifiers
    using type = _Ty;

    template <template <class> class _Fn>
    using _Apply = _Fn<_Ty>; // apply cv-qualifiers from the class template argument to _Fn<_Ty>
;

template <class _Ty>
struct remove_cv<const _Ty> 
    using type = _Ty;

    template <template <class> class _Fn>
    using _Apply = const _Fn<_Ty>;
;

template <class _Ty>
struct remove_cv<volatile _Ty> 
    using type = _Ty;

    template <template <class> class _Fn>
    using _Apply = volatile _Fn<_Ty>;
;

template <class _Ty>
struct remove_cv<const volatile _Ty> 
    using type = _Ty;

    template <template <class> class _Fn>
    using _Apply = const volatile _Fn<_Ty>;
;

template <class _Ty>
using remove_cv_t = typename remove_cv<_Ty>::type;


实战分析:is_void

上面讲了一些类型萃取的关键函数,下面就来实际看一看它们到底是怎么运作的,首先选取最简单的is_void来分析。

// STRUCT TEMPLATE is_void
template <class _Ty>
_INLINE_VAR constexpr bool is_void_v = is_same_v<remove_cv_t<_Ty>, void>;

template <class _Ty>
struct is_void : bool_constant<is_void_v<_Ty>> ;

我们从内往外开始分析,它的执行流程如下

  1. 调用remove_cv_t去除const与volatile属性。
  2. 借助is_same_v模板,判断参数类型_Ty与void是否相同。
  3. 将判断的结果作为bool_constant的参数,此时bool_constant类型为integral_constant<bool,result>
  4. is_void继承自指定版本的bool_constant

下面我们来实际使用一下

#include<iostream>
#include<type_traits>
using namespace std;

int main()

	cout << typeid(is_void<int>::type).name() << endl;
	cout << is_void<int>::value << endl;
	
	cout << typeid(is_void<void>::type).name() << endl;
	cout << is_void<void>::value << endl;

	return 0;


------------输出结果------------
struct std::integral_constant<bool,0>
0
struct std::integral_constant<bool,1>
1

至于其他一些类型的萃取函数,其实都与上面的大致相同,唯一不同的地方就是is_type_v这里的类型判断的逻辑,例如:

template <class _Ty>
struct is_union : bool_constant<__is_union(_Ty)> ; // determine whether _Ty is a union

template <class _Ty>
_INLINE_VAR constexpr bool is_union_v = __is_union(_Ty);

// STRUCT TEMPLATE is_class
template <class _Ty>
struct is_class : bool_constant<__is_class(_Ty)> ; // determine whether _Ty is a class

template <class _Ty>
_INLINE_VAR constexpr bool is_class_v = __is_class(_Ty);

// STRUCT TEMPLATE is_fundamental
template <class _Ty>
_INLINE_VAR constexpr bool is_fundamental_v = is_arithmetic_v<_Ty> || is_void_v<_Ty> || is_null_pointer_v<_Ty>;

template <class _Ty>
struct is_fundamental : bool_constant<is_fundamental_v<_Ty>> ; // determine whether _Ty is a fundamental type

遗憾的是C++标准库中并没有给出这些复杂类型的判断代码,在这里也就不过多的进行讲解了。


以上是关于还搞不懂STL的type_traits?从源码来带你一起分析的主要内容,如果未能解决你的问题,请参考以下文章

「底层原理」epoll源码分析,还搞不懂epoll的看过来

还搞不懂闭包算我输(JS 示例)

别告诉我Java8都出来这么久了,你还搞不懂Stream的map和flatmap的区别?

还搞不懂RS485?18个问答彻底讲明白RS485

2021面试还搞不懂Linux?快看看这阿里P8出的25道Linux面试常问问题!

Http 状态码 1xx 2xx 3xx 4xx 5xx 还搞不懂?直接撸 HTTP Protocol 吧