type-traits源码分析-三


title: type-traits源码分析(三)
date: 2022-09-28 10:34:42
tags:
- Modern C++
- C++
- C++ Library

在第三章中,会有一些新的东西,不同于一二章,TMP开始加速了!你将会看见额外的一些元函数实现手法,你肯定会惊讶它的巧妙,特别是void_t类型。总之,做好准备,这章的难度可不同于以往了!

实现

__is_one_of

传入一个类型T和一个参数包Ts,返回其T是不是Ts其中的一个类型,如果是,返回值/类型为 true/true_type, 否则返回false/false_type。

有了这个type_traits,可以更加轻松的实现其他type_traits。

这个type_traits的实现方式也是比较的有意思。我们可以有很多种方式来实现__is_one_of这个元函数,包括使用模板递归继承的方式来实现。gcc则是采用的参数包模式展开的方式。

  template
    using __is_one_of = __or_...>; //用is_same这个模式来对参数包展开,相当于每个类型都和_Tp应用于is_same,再用作于__or_,即可获得结果。

__is_signed/unsinged_integer

入一个类型,返回其是不是,如一个有符号/无符号的整数果是,返回值/类型为 true/true_type, 否则返回false/false_type。

  // Check if a type is one of the signed integer types.
  template
    using __is_signed_integer = __is_one_of<__remove_cv_t<_Tp>, //继承__is_one_of<符合要求的类型参数包>
	  signed char, signed short, signed int, signed long,
	  signed long long
#if defined(__GLIBCXX_TYPE_INT_N_0)			//条件编译大可忽略
	  , signed __GLIBCXX_TYPE_INT_N_0
#endif
#if defined(__GLIBCXX_TYPE_INT_N_1)
	  , signed __GLIBCXX_TYPE_INT_N_1
#endif
#if defined(__GLIBCXX_TYPE_INT_N_2)
	  , signed __GLIBCXX_TYPE_INT_N_2
#endif
#if defined(__GLIBCXX_TYPE_INT_N_3)
	  , signed __GLIBCXX_TYPE_INT_N_3
#endif
	  >;		

实现方式大同小异

  // Check if a type is one of the unsigned integer types.
  template
    using __is_unsigned_integer = __is_one_of<__remove_cv_t<_Tp>,
	  unsigned char, unsigned short, unsigned int, unsigned long,
	  unsigned long long
#if defined(__GLIBCXX_TYPE_INT_N_0)
	  , unsigned __GLIBCXX_TYPE_INT_N_0
#endif
#if defined(__GLIBCXX_TYPE_INT_N_1)
	  , unsigned __GLIBCXX_TYPE_INT_N_1
#endif
#if defined(__GLIBCXX_TYPE_INT_N_2)
	  , unsigned __GLIBCXX_TYPE_INT_N_2
#endif
#if defined(__GLIBCXX_TYPE_INT_N_3)
	  , unsigned __GLIBCXX_TYPE_INT_N_3
#endif
	  >;

__is_standard_integer

传入一个类型,返回其是不是标准定义的整数,如果是,返回值/类型为 true/true_type, 否则返回false/false_type。

这个type_traits是其他type_traits组合起来的一个别名。这也是元函数的一个实现手法。

  // Check if a type is one of the signed or unsigned integer types.
  template
    using __is_standard_integer
      = __or_<__is_signed_integer<_Tp>, __is_unsigned_integer<_Tp>>;

void_t

void_t,就是一个void的别名,但是带有类型参数。你会说:这不是脱裤子放屁吗?有什么用,我直接写void不好吗?

实际上void_t是为TMP服务的,并不是让你当作返回值/类型声明的。我们可以通过SFINE+void_t这种TMP的手法来实现看似不可能的元函数。你马上就会看到

至于你说为什么叫void_t而不是叫其他类型,那你去问问C++委员会吧,顺便告诉我,我也想知道。

  // __void_t (std::void_t for C++11)
  template using __void_t = void;

__is_referenceable

传入一个类型,返回其是不是可引用的,如果是,返回值/类型为 true/true_type, 否则返回false/false_type。

需要注意:一个类型T,如果已经是一个引用,无论是左值还是右值,均不可再添加&,就是说没有(int &) &这样的语法,即引用的引用。但是在模板中,有引用的折叠,别把这件事情忘了。

拿什么是不可引用的呢?

  • void
  • 例如int () const这样的函数
  // Utility to detect referenceable types ([defns.referenceable]).

  template //第二个类型参数用于辅助,不需要有名字。
    struct __is_referenceable
    : public false_type
    { };

  template
    struct __is_referenceable<_Tp, __void_t<_Tp&>> //如果是一个不可引用的类型,匹配这个特例化失败,转而匹配主模板
    : public true_type
    { };

一些特殊的类型

C++中有一些类型,具有独特的语义。例如 无其他动作的构造。内存布局方式是标准的。无其他动作的拷贝。一个抽象基类,一个被final修饰的类,等等deng’den。想对这些类型实现type_traits不是不行,但是编译器为我们内置这样的功能可能会更好,编译速度也会更加快速。

trivial

一个trivial需要具有一下的性质

  • 有默认构造函数,编译器生成的,或者是显示的=default,只能是这两种情况。
  • 内存空间是连续的
  • 不能包含虚函数和虚基类。
  • 成员可以具有不同的访问权限
  • 拥有平凡的拷贝构造函数和移动构造函数。默认的意思同上,也可以使用=default。
  • 拥有平凡的拷贝赋值运算符和移动赋值运算符。

stand_layout

一个stand_layout的对象,需要满足以下几种性质

  • 所有非静态成员拥有相同的访问级别
  • 类中第一个非静态类型与基类不是同一个类型
  • 所有非静态数据成员都符合标准布局的要求
  • 没有虚类和虚基类
  • 继承时需要满足以下两个情况之一
    1. 派生类中有非静态类,那么这个派生类只能有且只有一个仅包含了静态成员的基类。
    2. 基类有非静态成员,那么派生类中不允许有非静态成员。

ps:这里涉及到C++对象模型,感兴趣的可以自行学习。

pod

pod类型需要满足两个要求

  • 是一个人trivial类型
  • 是一个stand_layout类型

pod类型的一些优势/特点

  • POD类型兼容C内存布局,C++可以直接使用C库函数操作POD数据类型,POD类型在C和C++间的操作总是安全的
  • POD类型可以直接使用字节赋值,使用C语言库函数进行二进制形式的数据交换,包括但不限于如下操作memmove,memcpy
  • 静态初始化在很多时候可以提高程序性能,而POD类型的静态初始化非常简单(放入目标文件的.bss段,在初始化时直接赋0)
  • 虽然与C完全兼容,但是仍然可以有成员函数
  • 有更长的生命周期(从资源获取到资源释放),非POD类型的生命周期从构造函数结束到析构函数结束
  • POD类型对象的前部没有填充字节,因此对象指针等于对象第一个成员的指针

ps:还是涉及到C++对象模型,有点头秃,大伙看看就好,毕竟术业有专攻。编写应用层C++/非底层库看看就好。

STL的一些算法为了优化性能就会使用is_pod+标签分派的手法来优化性能。在一些很老的标准库中,你会看见对is_pod的手动实现

literal_type

简单的说,就可以在用于编译期运算的对象。注意其和字面量的区别。

还有一些编译器为我们内置的type_traits

这些type_traits的共呢个正如他们的名字所言,你唯一搞不懂的,应当是什么是POD类型?什么又是trivially_copyable性质?

 /// is_trivial
//普通类型是其存储是连续的普通可复制的,并且仅支持静态默认初始化,无论是否为 cv 限定。它包括标量类型,平凡类和任何此类类型的数组。其实现也是编译器内置的。
  template
    struct is_trivial 
    : public integral_constant
    {
      static_assert(std::__is_complete_or_unbounded(__type_identity<_Tp>{}), //这里的静态断言,我们在前面说过了,不再解释,下同。
	"template argument must be a complete class or an unbounded array");
    };

  // is_trivially_copyable
  template
    struct is_trivially_copyable 	//是否具有平凡的拷贝
    : public integral_constant
    {
      static_assert(std::__is_complete_or_unbounded(__type_identity<_Tp>{}),
	"template argument must be a complete class or an unbounded array");
    };

  /// is_standard_layout
  template
    struct is_standard_layout	//是否是一个标准布局
    : public integral_constant
    {
      static_assert(std::__is_complete_or_unbounded(__type_identity<_Tp>{}),
	"template argument must be a complete class or an unbounded array");
    };

  /// is_pod (deprecated in C++20) C++20中已经废弃
  // Could use is_standard_layout && is_trivial instead of the builtin.
  template	
    struct
    _GLIBCXX20_DEPRECATED("use is_standard_layout && is_trivial instead")
    is_pod
    : public integral_constant
    {
      static_assert(std::__is_complete_or_unbounded(__type_identity<_Tp>{}),
	"template argument must be a complete class or an unbounded array");
    };

  /// is_literal_type
  //17开始 20弃用
  template
    struct
    _GLIBCXX17_DEPRECATED
    is_literal_type 		//是否是一个字面类型
    : public integral_constant
    {
      static_assert(std::__is_complete_or_unbounded(__type_identity<_Tp>{}),
	"template argument must be a complete class or an unbounded array");
    };

  /// is_empty
  template
    struct is_empty			//是否是一个空类型,即类中为空实现
    : public integral_constant
    { };

  /// is_polymorphic
  template
    struct is_polymorphic		// 是否是一个多态类
    : public integral_constant
    { };

#if __cplusplus >= 201402L
#define __cpp_lib_is_final 201402L
  /// is_final
  template
    struct is_final		
    : public integral_constant
    { };
#endif

  /// is_abstract
  template
    struct is_abstract
    : public integral_constant
    { };

is_signed

template::value> //默认参数是一个bool值,来判断书不是算数类型
  struct __is_signed_helper
  : public false_type { };

template
  struct __is_signed_helper<_Tp, true>	//蕴含着是一个符号类型应当先是算数类型
  : public integral_constant	//如果是一个有符号类型,那么_Tp(-1) < _Tp(0)得到的结果将会是false,是一个无符号类型,-1,会下溢结果为true。
  { };

/// is_signed
template
  struct is_signed
  : public __is_signed_helper<_Tp>::type
  { }

is_unsigned

  /// is_unsigned
  template
    struct is_unsigned
    : public __and_/*也可以不加这个元函数,is_singed中有*/, __not_>> //算数类型的同时对is_singed取反
    { };

decltype手法

C++11中新增了decltype关键字,目的就是解决TMP中的一些问题。decltype会对表达式求值,只会根据表达式来推导类型,这点牢记。我不打算说明decltype的所有细节,你可以查阅资料来了解相关知识。decltype为TMP的实现提供了更多的可能性。decltype+SFINAE也是TMP中的一种很巧妙的手法。

declval

declval是一个函数,只有声明,没有实现,目的很简单,服务于TMP。因此,关于declval的所有操作,均在编译时期发生。

  /**
   *  @brief  Utility to simplify expressions used in unevaluated operands
   *  @ingroup utilities
   */
//提供了两个的重载版本
  template
    _Up
    __declval(int);

  template
    _Tp
    __declval(long);

  template
    auto declval() noexcept -> decltype(__declval<_Tp>(0)); //尾置返回类型

extent

如果T是数组类型,则提供成员常量等于数组第nth维的元素数。或者T是其他类型,或者数组的第0维数为无界的并且nth=0,则值为0。

你可能会疑惑为什么第0维是无界的时候值为0,第1,2,…维不行吗?别忘了C的语法——要确定数组的列数。

  /// extent
  template	
    struct extent
    : public integral_constant { };	  //T不是数组类型,值为0

  template
    struct extent<_Tp[_Size], _Uint>
    : public integral_constant::value>	//递归条件_Uint == 0,在_Uint==0的时候,编译器推断的size正好是nth的长度,实现很巧妙,多看几眼慢慢搞清顺序。
    { };

  template
    struct extent<_Tp[], _Uint>				//如果是一个无界数组
    : public integral_constant::value> 
    { };

用法:

int main() {
    int arr [][3][2] = {};
    cout << std::extent::value << endl;  //0
    cout << std::extent::value << endl;  //3
    cout << std::extent::value << endl;  //5
    int arr1[1][2];
    cout << std::extent::value << endl;   //1
    cout << std::extent::value << endl;   //2
    
    //decltype(arr)的结果为什么超出我么的预期?gcc出bug了吗?
    cout << std::extent::value << endl;    //0
    cout << std::extent::value << endl;    //0
    cout << std::extent::value << endl;    //0
    return 0;
}

其实decltype的推导arr的结果为[0][3][2]。是不同于直接传递int[][3][5]的。decltype的推断出一个有界数组,而直接传递arr,arr被当作一个无界数组。

rank

传入一个类型,返回其数组的维数。

同样是使用递归+继承的方式来实现。

  /// rank
  template
    struct rank
    : public integral_constant { };

  template
    struct rank<_Tp[_Size]>
    : public integral_constant::value> { };

  template
    struct rank<_Tp[]>
    : public integral_constant::value> { };

总结

本章更加深入的了解了type_traits的更加高阶的实现技巧。但这还只是TMP的冰山一角而已。无论难度如何,总之,还能够接受。介绍了一些维TMP服务的基本元函数,例如declval等,虽然定义很短,但是很复杂也巧妙。

下一章,会更加深入的分析type_traits,前三章分析的type_traits的耦合度很低,很多的tyep_traits都相对独立。而后些章节。那些type_traits都需要很多的元函数做支撑。并且还会有更加复杂的TMP实现手法等着你。

至此,type_traits的源码差不多分析的近900行,还有剩下的2000多行等待我们探索。

你可能感兴趣的:(C++,TMP,c++,后端,C++标准库,模板元编程)