C++ template —— 类型区分
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ template —— 类型区分相关的知识,希望对你有一定的参考价值。
前面的博文介绍了模板的基础,深入模板特性,模板和设计的一些内容。从这篇开始,我们介绍一些高级模板设计,开发某些相对较小、并且互相独立的功能,而且对于这些简单功能而言,模板是最好的实现方法:
(1)一个用于类型区分的框架;
(2)智能指针
(3)tuple
(4)仿函数
------------------------------------------------------------------------------------------------------------
第19章 类型区分
本章主要介绍用模板实现对类型的辨识,判断其是内建类型、指针类型、class类型或者其他类型中的哪一种。
------------------------------------------------------------------------------------------------------------
19.1 辨别基本类型
缺省情况下,我们一方面假定一个类型不是一个基本类型,另一方面我们为所有的基本类型都特化给模板:
// types/type1.hpp // 基本模板:一般情况下T不是基本类型 template <typename T> class IsFundaT { public: enum { Yes = 0, No = 1 }; }; // 用于特化基本类型的宏 #define MK_FUNDA_TYPE(T) \ template<> class IsFundaT<T> { public: enum { Yes = 1, No = 0 }; }; MK_FUNDA_TYPE(void) MK_FUNDA_TYPE(bool) MK_FUNDA_TYPE(char) MK_FUNDA_TYPE(signed char) MK_FUNDA_TYPE(unsigned char) MK_FUNDA_TYPE(wchar_t) MK_FUNDA_TYPE(signed short) MK_FUNDA_TYPE(unsigned short) MK_FUNDA_TYPE(signed int) MK_FUNDA_TYPE(unsigned int) MK_FUNDA_TYPE(signed long) MK_FUNDA_TYPE(unsigned long) #if LONGLONG_EXISTS MK_FUNDA_TYPE(signed long long) MK_FUNDA_TYPE(unsigned long long) #endif // LONGLONG_EXISTS MK_FUNDA_TYPE(float) MK_FUNDA_TYPE(double) MK_FUNDA_TYPE(long double) #undef MK_FUNDA_TYPE
19.2 辨别组合类型
组合类型是指一些构造自其他类型的类型。简单的组合类型包括:普通类型、指针类型、引用类型和数组类型。它们都是构造自单一的基本类型。同时,class类型和函数类型也是组合类型,但这些组合类型通常都会涉及到多种类型(例如参数或者成员的类型)。在此,我们先考虑简单的组合类型;另外,我们还将使用局部特化对简单的组合类型进行区分。接下来,我们将定义一个trait类,用于描述简单的组合类型;而class类型和枚举类型将在最后考虑。
// types/type2.hpp template <typename T> class CompoundT // 基本模板 { public: enum { IsPtrT = 0, IsRefT = 0, IsArrayT = 0, IsFuncT = 0, IsPtrMemT = 0 }; typedef T BaseT; typedef T BottomT; typedef CompoundT<void> ClassT; };
成员类型BaseT指的是:用于构造模板参数类型T的(直接)类型;而BottomT指的是最终去除指针、引用和数组之后的、用于构造T的原始类型。例如,如果T是int**,那么BaseT将是int*,而BottomT将会是int类型。对于成员指针类型,BaseT将会是成员的类型,而ClassT将会是成员所属的类的类型。例如,如果T是一个类型为int(X::*)()的成员函数指针,那么BaseT将会是函数类型int(),而ClassT的类型则为X。如果T不是成员指针类型,那么ClassT将会是CompoundT<void>(这个选择并不是必须的,也可以使用一个noclass来作为ClassT)。
其中,针对指针和引用的局部特化是相当直接的:
// types/type3.hpp template <typename T> class CompoundT<T&> { public: enum { IsPtrT = 0, IsRefT = 1, IsArrayT = 0, IsFuncT = 0, IsPtrMemT = 0 }; typedef T BaseT; typedef typename CompoundT<T>::BottomT BottomT; typedef CompoundT<void> ClassT; }; template <typename T> class CompoundT<T*> { public: enum { IsPtrT = 1, IsRefT = 0, IsArrayT = 0, IsFuncT = 0, IsPtrMemT = 0 }; typedef T BaseT; typedef typename CompoundT<T>::BottomT BottomT; typedef CompoundT<void> ClassT; };
对于成员指针和数组,我们可能会使用同样的技术来处理。但是,在下面的代码中我们将发现,与基本模板相比,这些局部特化将会涉及到更多的模板参数:
// types/type4.hpp #include <stddef.h> template<typename T, size_t N> class CompoundT<T[N]> { public: enum { IsPtrT = 0, IsRefT = 0, IsArrayT = 1, IsFuncT = 0, IsPtrMemT = 0 }; typedef T BaseT; typedef typename CompoundT<T>::BottomT BottomT; typedef CompoundT<void> ClassT; }; template<typename T> class CompoundT<T[]> { public: enum { IsPtrT = 0, IsRefT = 0, IsArrayT = 1, IsFuncT = 0, IsPtrMemT = 0 }; typedef T BaseT; typedef typename CompoundT<T>::BottomT BottomT; typedef CompoundT<void> ClassT; }; template<typename T> class CompoundT<T C::*> { public: enum { IsPtrT = 0, IsRefT = 0, IsArrayT = 0, IsFuncT = 0, IsPtrMemT = 1 }; typedef T BaseT; typedef typename CompoundT<T>::BottomT BottomT; typedef C ClassT; };
19.3 辨别函数类型
书中提供了两种辨识函数类型的方法,这里只介绍第2种:
method2:使用SFINAE原则的解决方案:
一个重载函数模板的后面可以是一些显式模板实参;而且对于某些重载函数类型而言,该实参是有效的,但对于其他的重载函数类型,该实参则可能是无效的。实际上,后面使用重载解析对枚举类型进行辨别的技术也使用到了这种方法。SFINAE原则在这里的主要用处是:(1)找到一种构造,该构造对函数类型是无效的,但是对于其他类型都是有效的;或者完全相反。由于前面我们已经能够辨别出几种类型了,所以我们在此可以不再考虑这些(已经可以辨别的)类型。(2)因此,针对上面这种要求,数组类型就是一种有效的构造;因为数组的元素是不能为void值、引用或者函数的。故而可以编写如下代码:
template <typename T> class IsFunctionT { private: typedef char One; typedef struct { char a[2]; } Two; template <typename U> static One test ( ... ); template <typename U> static Two test ( U (*)[1] ); // 不理解,下面的IsFunctionT<T>::test<T>(0)怎么匹配? public: enum { Yes = sizeof(IsFunctionT<T>::test<T>(0)) == 1 }; enum { No = !Yes }; };
借助于上面这个模板定义,只有对于那些不能作为数组元素类型的类型,IsFunctionT::Yes才是非零值(即为1)。另外,我们应该知道该方法也有一个不足之处;并非只有函数类型不能作为数组元素类型,引用类型和void类型同样也不能作为数组元素类型。(3)幸运的是,我们可以通过为引用类型提供局部特化,以及为void类型提供显式特化,来解决这个不足:
template <typename T> class IsFunctionT<T&> { public: enum { Yes = 0 }; enum { No = !Yes }; }; template <> class IsFunctionT<void> { public: enum { Yes = 0 }; enum { No = !Yes }; }; template <> class IsFunctionT<void const> { public: enum { Yes = 0 }; enum { No = !Yes }; };
至此,我们可以重新改写基本的CompoundT模板如下:
// types/type6.hpp template <typename T> class IsFunctionT { private: typedef char One; typedef struct { char a[2]; } Two; template <typename U> static One test ( ... ); template <typename U> static Two test ( U (*)[1] ); public: enum { Yes = sizeof(IsFunctionT<T>::test<T>(0)) == 1 }; enum { No = !Yes }; }; template <typename T> class IsFunctionT<T&> { public: enum { Yes = 0 }; enum { No = !Yes }; }; template <> class IsFunctionT<void> { public: enum { Yes = 0 }; enum { No = !Yes }; }; template <> class IsFunctionT<void const> { public: enum { Yes = 0 }; enum { No = !Yes }; }; // 对于void volatile 和 void const volatile类型也是一样的 ... template <typename T> class CompoundT // 基本模板 { public: enum { IsPtrT = 0, IsRefT = 0, IsArrayT = 0, IsFuncT = IsFunctionT<T>::Yes, IsPtrMemT = 0 }; typedef T BaseT; typedef T BottomT; typedef CompoundT<void> ClassT; };
19.4 运用重载解析辨别枚举类型
重载解析是一个过程,它会根据函数参数的类型,在多个同名函数中选择出一个合适的函数。接下来我们将看到,即使没有进行实际的函数调用,我们也能够利用重载解析来确定所需要的结果。总之,对于测试某个特殊的隐式转型是否存在的情况,这种(利用重载解析的)方法是相当有用的。在此,我们将要利用从枚举类型到整型的隐式转型:它能够帮助我们分辨枚举类型。
先看实现:
// types/type7.hpp struct SizeOverOne { char c[2]; }; template<typename T, bool convert_possible = !CompoundT<T>::IsFuncT && !CompoundT<T>::IsArrayT> class ConsumeUDC { public: //在ConsumeUDC模板中已经强制定义了一个到T的自定义转型 operator T() const; }; // 到函数类型的转型是不允许的 // 如果由基本模板得到的convert_possible为false,则匹配此特化;不转型-->无自定义转型操作 template<typename T> class ConsumeUDC<T, false> { }; // 到void类型的转型是不允许的 template <bool convert_possible> class ConsumeUDC<void, convert_possible> { }; char enum_check(bool); char enum_check(char); char enum_check(signed char); char enum_check(unsigned char); char enum_check(wchar_t); char enum_check(signed short); char enum_check(unsigned short); char enum_check(signed int); char enum_check(unsigned int); char enum_check(signed long); char enum_check(unsigned long); #if LONGLONG_EXISTS char enum_check(signed long long); char enum_check(unsigned long long); #endif // LONGLONG_EXISTS // 避免从float到int的意外转型 char enum_check(float); char enum_check(double); char enum_check(long double); SizeOverOne enum_check( ... ); // 捕获剩余所有情况 template<typename T> class IsEnumT { public: enum { Yes = IsFundaT<T>::No && !CompoundT<T>::IsRefT && !CompoundT<T>::IsPtrT && !CompoundT<T>::IsPtrMemT && sizeof(enum_check(ConsumeUDC<T>())) == 1 } enum { No = !Yes }; };
上面代码的核心在于后面的一个sizeof表达式,它的参数是一个函数调用。也就是说,该sizeof表达式将会返回函数调用返回值的类型的大小;其中,将应用重载解析原则来处理enum_check()调用;但另一方面,我们并不需要函数定义,因为实际上并没有真正调用该函数。在上面的例子中,如果实参可以转型为一个整型,那么enum_check()将返回一个char值,其大小为1。对于其他的所有类型,我们使用了一个省略号函数(即enum_check( ... ) ),然而,根据重载解析原则的优先顺序,省略号函数将会是最后的选择。在此,我们对enum_check()的省略号版本进行了特殊的处理,让它返回一个大小大于一个字节的类型(即SizeOverOne)。
对于函数enum_check的调用实参,我们必须仔细地考虑。首先,我们并不知道T是如何构造的,或许将会调用一个特殊的构造函数。为了解决这个问题,我们可以声明一个返回类型为T的函数,然后通过调用这个函数来创建一个T。由于处于sizeof表达式内部,因此该函数实际上并不需要具有函数定义。事实上,更加巧妙的是:对于一个class类型T,重载解析是有可能选择一个针对整型的enum_check()声明的,但前提是该class必须定义一个到整型的自定义转型(有时也称为UDC)函数。到此,问题已经解决了。因为我们在ConsumeUDC模板中已经强制定义了一个到T的自定义转型,该转型运算符同时也为sizeof运算符生成了一个类型为T的实参。下面我们详细分析下:
(1)最开始的实参是一个临时的ConsumeUDC<T>对象;
(2)如果T是一个基本整型,那么将会借助于(ConsumeUDC的)转型运算符来创建一个enum_check()的匹配,该enum_check()以T为实参;
(3)如果T是一个枚举类型,那么将会借助于(ConsumeUDC的)转型运算符,先把类型转化为T,然后调用(从枚举类型到整型的)类型提升,从而能够匹配一个接收转型参数的enum_check()函数(通常而言是enum_check(int));
(4)如果T是一个class类型,而且已经为该class自定义了一个到整型的转型运算符,那么这个转型运算符将不会被考虑。因为对于以匹配为目的的自定义转型而言,最多只能调用一次;而且在前面已经使用了一个从ConsumeUDC<T>到T的自定义转型,所以也就不允许再次调用自定义转型。也就是说,对enum_check()函数而言,class类型最终还是未能转型为整型。
(5)如果最终还是不能让类型T于整型互相匹配,那么将会选择enum_check()函数的省略号版本。
最后,由于我们这里只是为了辨别枚举类型,而不是基本类型或者指针类型,所有我们使用了前面已经开放的IsFundaT和CompoundT类型,从而能够排除这些令IsEnumT<T>::Yes成为非零的其他类型,最后使得只有枚举类型的IsEnumT::Yes才等于1。
19.5 辨别class类型
使用排除原理:如果一个类型不是一个基本类型,也不是枚举类型和组合类型,那么该类型就只能是class类型。
template <typename T> class IsClassT { public: enum { Yes = IsFundaT<T>::No && IsEnumT<T>::No && !CompoundT<T>::IsPtrT && !CompoundT<T>::IsRefT && !CompoundT<T>::IsArrayT && !CompoundT<T>::IsPtrMemT && !CompoundT<T>::IsFuncT }; enum { No = !Yes }; };
以上是关于C++ template —— 类型区分的主要内容,如果未能解决你的问题,请参考以下文章
4万字c++讲解+区分c和c++,不来可惜了(含代码+解析)
小白学习C++ 教程十五C++ 中的template模板和泛型