还搞不懂STL的type_traits?从源码来带你一起分析!!!
Posted 凌桓丶
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了还搞不懂STL的type_traits?从源码来带你一起分析!!!相关的知识,希望对你有一定的参考价值。
文章目录
什么是类型萃取?
type_traits
被称为类型萃取,主要用于在编译期计算、查询、判断、转换和选择,增强了泛型编程的能力,也增强了程序的弹性,使得我们在编译期就能做到优化改进甚至排错,能进一步提高代码质量。
cplusplus-type_traits
在C++中类型萃取主要包含三大部分
-
辅助类(Helper classes)
- 用于帮助创建编译器常量的标准类。
- 用于帮助创建编译器常量的标准类。
-
类型萃取(Type traits)
- 在编译期以常量的形式获取类的特征。
- 在编译期以常量的形式获取类的特征。
-
类型转换(Type transformations)
- 为某个类型增加/删除属性。例如增加/删除const、volatile等。
- 为某个类型增加/删除属性。例如增加/删除const、volatile等。
源码剖析
以下代码全部来自C++标准库的type_traits
、xtr1common
,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()
等方法来以最高效率执行。
如果我们准备对一个类型未知的数组进行拷贝操作时,如果能够提前知道它使用的是默认的拷贝构造函数,就可以直接使用memcpy
、memmove
来快速进行拷贝,对于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
为了能利用模板来实现如or
、and
等逻辑判断,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_constant
与bool_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_const
和remove_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>> {};
我们从内往外开始分析,它的执行流程如下
- 调用
remove_cv_t
去除const与volatile属性。 - 借助
is_same_v
模板,判断参数类型_Ty与void是否相同。 - 将判断的结果作为
bool_constant
的参数,此时bool_constant
类型为integral_constant<bool,result>
。 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?从源码来带你一起分析!!!的主要内容,如果未能解决你的问题,请参考以下文章
别告诉我Java8都出来这么久了,你还搞不懂Stream的map和flatmap的区别?