在C++中侦测内嵌类型的存在(rev#2)
By 刘未鹏(pongba)
C++的罗浮宫(http://blog.csdn.net/pongba)
动机(Motivation)
假设一所大学的注册系统提供了一个注册函数:
template<class T>
void Register(T person)
{
Register(person, typename T::person_tag());
};
而对于注册者有以下几种标识:
struct student_tag{};
struct teacher_tag{};
还有Register的几个供内部使用的重载版本:
template<class T> void Register(T p, student_tag){...} // 注册学生
template<class T> void Register(T p, teacher_tag){...} // 注册教师
并规定学生类一定要在内部typedef student_tag person_tag,教师类typedef teacher_tag person_tag,这样,当传给起初的那个Register的对象为学生类对象时,typename T::person_tag()其实就构造了一个student_tag对象,从而激发函数重载,调用Register内部版本的template<class T> void Register(T p, student_tag)版本。其他情况亦均有对应。这是泛型编程里的常用手法(静态多态),STL里屡见不鲜。
问题是,现在学校里假如不止学生教师,还有工人,警卫等其它人员。如果他们不会在类内部typedef任何东西,则Register需要一种机制以确定T内部是否typedef了某个标识符(例如person_tag)。如果没有,就默认处理。如果有,则再进行更详细的分类。
实现(Implementation)
这个问题可能有两个实现途径。
一是利用函数重载,具体如下:
typedef char (&yes_type)[1]; // sizeof(yes_type)==1
typedef char (&no_type)[2]; // sizeof(no_type)==2
以上的两个typedef用于识别不同的重载函数。char (&)[1]表示对char[1]数组的引用,所以sizeof(char(&)[1])==sizeof(char[1])==1。注意围绕&符号的一对圆括号,它们是必要的,如果没有将会导致编译错误,正如char* [1]将被解析为char*的数组,char& [1]将被解析为引用的数组,而后者是非法的。将&用圆括号包围则改变了运算符的结合优先序,这将被解析为对char[1]数组的引用。
template<class T>
struct does_sometypedef_exists
{
template<class U>
static yes_type check(U, typename U::key_type* =0); // #1
static no_type check(...);
static T t; // 声明
static const bool value = sizeof(check(t))==sizeof(yes_type);
};
注意,#1处,*和=之间的空格是必要的,否则编译器会将它解析为operator*=操作符。
在我的VC7.0环境下,以下测试是成功的:
struct A{};
struct B
{
typedef int key_type;
};
int main()
{
std::cout << does_sometypedef_exists<A>::value<<' ' // 0
<< does_sometypedef_exists<B>::value<<' ' // 1
<< std::endl;
};
下面我为你讲解它的原理。
当进行重载解析时,编译器会首先尝试实例化可以匹配的模板函数并将它们纳入到有待进行重载解析的函数的候选单之列,在本例中,当typename T::key_type不存在时,check的第一个模板版本不能实例化(因为其第二个参数类型typename U::key_type*不存在),所以只能匹配第二个版本。当typename T::key_type存在时,第一个模板函数可以实例化,且可以匹配(注意第二个参数为缺省参数),所以无疑编译器会匹配第一个版本,因为C++标准保证:只有当其它所有重载版本都不能匹配的时候含有任意类型参数列表的版本(在本例中那是no_type check(...))才会被匹配。
一个值得注意的地方是:check的第一个版本只能是模板函数,因为当编译器推导类型的过程中发现该模板函数不能实例化时它就不去实例化它,而不是产生编译错误(除非没有其它可匹配的重载版本)。因为编译错误只有将代码编译的过程中才会产生,而既然模板没有实例化,那么该模板实际上并没有经过编译。
然而,如果它不是模板函数,则随着does_sometypedef_exists类的实例化。它也会被实例化,然而如果不存在T::key_type,那么,该函数就成为非法。
还有一个值得注意的地方是:does_sometypedef_exists内部的static T t;只是一个声明,并不占用内存空间,更妙的是,因为是个声明,所以编译器根本不会对它初始化,所以它的默认构造函数就根本不会被执行,事实上,编译器在这种情况下甚至不会去看一看它是否有可用的默认构造函数,它只需要类型信息就足够了,不是么?因此,即使由于某些原因(例如,想让T从堆上创建)T的默认构造函数被禁止(设为private),那么以上的traits也不会通不过编译。“但是,等等!”你仿佛意识到了问题:“check的参数是传值的!这时如果T的拷贝构造函数是私有的将会发生什么事情呢?”事实是,根本不用去担心,在sizeof的世界里,根本不会发生求值行为,编译器只需要有关类型的信息。在编译器内部蕴涵有一个巨大的类型推导系统。无论sizeof(...)里的表达式多么复杂,其类型都会最终在编译期被正确推导出来。而对于sizeof(check(t)),编译器有了函数的返回值类型信息就够了,它并不会去执行函数的代码,也不会做实际的传参行为,所以拷贝构造也就无从发生。
但这里有一个十分怪异的问题(在我的VC7.0环境下存在),假设我们增加一个新类:
struct C
{
template<class T>
struct key_type{}; // 请注意这是个模板类
};
按理说,这种情况下does_sometypedef_exists<C>::value应该为false,因为第一个重载版本的typename U::key_type*不能被推导为C::key_type* (C::key_type是个模板,它需要模板参数来实例化),然而在我的VC7.0下它通过编译了,并且结果为true(就是说重载解析为第一个check函数)。如果我将check的第一个版本作一点小小的改动,像这样:
template<class U>
static yes_type check(U,
typename U::key_type* = (typename U::key_type*)0);
我仅仅加了一个转换,编译器就开始抱怨说使用模板类(它指的是C::key_type)需要模板参数了。我作了另外的种种测试(甚至我发现如果将10传给它的第二个参数,编译器会说不能将int转换为C::key_typ*,是的,这是编译错误的原文,这是否表示编译器承认C::key_type*为一种类型呢?我不知道)。结论是只有当typename U::key_type*作为模板函数的参数类型时这种情况才会发生。
第二种实现是利用模板偏特化及默认模板参数的规则:
template<class T,class>
struct check_helper
{
typedef T type;
};
template<class T,class =T>
struct does_sometypedef_exists_1
{
static const bool value=false;
};
template<class T>
struct does_sometypedef_exists_1<T,
typename check_helper<T, typename T::key_type>::type>
{
static const bool value=true;
};
这看起来很小巧,仅仅使用了模板偏特化。但是请耐心听我解释。
如果typename X::key_type存在(假设X为任意类),则does_sometypedef_exists_1<X>首先由模板推导将does_sometypedef_exists_1的模板参数T匹配为X,则其偏特化版本因而被推导为:
struct does_sometypedef_exists_1<X,
typename check_helper<X,typename X::key_type>::type>
而typename check_helper<X,typename X::key_type>::type根据check_helper的定义其实就是X,所以该偏特化版本其实被推导为:
struct does_sometypedef_exists_1<X,X>
所以,如果你这样测试:does_sometypedef_exists_1<X>::value,根据does_sometypedef_exists_1缺省定义(第二个模板参数默认为T),你写的相当于:does_sometypedef_exists_1<X, X>::value。
而根据上面的推导,如果typename X::key_type存在,则does_sometypedef_exists_1的偏特化版本也存在且形式为:
struct does_sometypedef_exists_1<X, X>
于是编译器选择匹配偏特化版本,其中的value值为true。
而如果typename X::key_type不存在,则typename check_helper<X, typename X::key_type>::type也就随之不存在,则does_sometypedef_exists_1的偏特化版本也就随之不存在,于是编译器会选择使用缺省定义,其中value值为false。这正是我们所想要的结果。
测试(Test)
现在对我们的两个实现版本测试一下吧,假设有一下几个类:
// 没有key_type
struct A{};
// typedef
struct B{typedef int key_type;};
// key_type为成员函数
struct C{void key_type(void){}};
// key_type为静态常量数据成员
struct D{static const bool key_type=false;};
// 定义,D里面的是声明
const bool D::key_type;
// key_type为模板类
struct E{
border-right: medium none; padding-right: 0cm; border-top: medium none; padding-left: 0cm; padding-bottom: 0cm; margin: 0cm 0cm 0pt; border-left: medium none; text-
分享到:
相关推荐
在C++编程中,有时我们需要检测一个类型是否定义了内嵌的枚举类型或typedef,以便根据这些类型的存在来进行特定的处理。这个问题在设计泛型库或者模板元编程时尤其重要,因为它允许我们编写更加灵活和自适应的代码。...
在给定的例子中,使用`sizeof(str)`获取字符串长度的方法存在缺陷,因为当字符串以数组形式传递时,`sizeof`得到的是数组大小而非实际字符串长度。 ### 12. 字符转十六进制的问题 代码中的问题在于没有处理非ASCII...
- **bool型别**:C++中新增加的布尔数据类型,使得逻辑判断更加直观和易于理解。 - **关键字explicit**:用于禁止隐式类型转换,提高代码的健壮性。 - **新的类型转换运算子**:增强了类型转换的灵活性和安全性。...
在C++实现中,非可视化的型别均为QObject的子类,可视化的类型均为QDeclarativeItem的子类。注意:QDeclarativeItem等同于QML的Item类。 如果用户想要定义自己的型别,做法如下: 在C++中,实现派生于QObject或...
书中展示了一些模板元编程的例子,比如通过模板进行型别安全的维度分析,这表明模板不仅仅可以用来生成容器类型,还可以在编译时进行复杂的类型操作,实现各种高级特性。 值得注意的是,书中还详细探讨了STL...
在C++中,非内建型别A和B之间,B能够隐式转化为A的条件有哪些? #### 解析: - **继承关系**:如果类B公有继承自类A,则可以将B类型的对象隐式转化为A类型。 ```cpp class B : public A { ... }; ``` - **转换...
问题:非 C++内建型别 A 和 B,在哪几种情况下 B 能隐式转化为 A? 答案:a. class B : public A { ……} // B 公有继承自 A,可以是间接继承的 b. class B { operator A(); } // B 实现了隐式转化为 A 的转化 c....
12.1.2 继承和派生如何在C++中实现 12.1.3 继承的种类及语法 12.1.4 单一继承 12.2 公有型、保护型和私有型 12.3 访问权限 12.4 多重继承 12.5 继承的构造与析构 12.6 合理利用基类构造函数 12.7 继承和重载...
[§2 C++的设计和演化]: 介绍C++语言的设计目标和其演化发展的原则; [§3 C程序设计模型]: 介绍C++所包含的C语言子集以及其它支持传统的系统程序设计风格的语言设施; [§4 C++的抽象机制]: 介绍类(class...
想要彻底理解C++11和C++14,不可止步于熟悉它们引入的语言特性(例如,auto型别推导、移动语义、lambda表达式以及并发支持)。...“旧”C++程序设计(即C++98)中的最佳实践要求在现代C++的软件开发中作出哪些...
### C++常见的99个错误知识点详解 #### 第一章 基础问题 **常见错误1:过分积极的注释** - **错误表现**:过度注释代码,特别是注释那些非常直观、易于理解的部分。 - **正确做法**:只在必要的地方添加注释,...
12.1.2 继承和派生如何在C++中实现 12.1.3 继承的种类及语法 12.1.4 单一继承 12.2 公有型、保护型和私有型 12.3 访问权限 12.4 多重继承 12.5 继承的构造与析构 12.6 合理利用基类构造函数 12.7 继承和重载的两义性...
在 C++ 中,类是对复杂对象的抽象表示。类的定义规则包括:类表示定义了一个新的类型和新的作用域。类对象表示具备属性和行为的对象。数据成员表示类中的数据,函数或类型别名。成员函数表示类中的操作函数和运算符...
[§2 C++的设计和演化]: 介绍C++语言的设计目标和其演化发展的原则; [§3 C程序设计模型]: 介绍C++所包含的C语言子集以及其它支持传统的系统程序设计风格的语言设施; [§4 C++的抽象机制]: 介绍类(class...
2. 非C++内建型别 A 和 B,在哪几种情况下 B 能隐式转化为 A? 这个问题是关于C++语言中的隐式类型转换。答案有四个: * B 公有继承自 A,可以是间接继承的。 * B 实现了隐式转化为 A 的转化。 * A 实现了 non-...
本书以4个面向来表现C++的本质:procedural(程序性的)、generic(泛型的)、object-based(个别对象的)、object-oriented(面向对象的)。本书的组织围绕着一系列逐渐繁复的程序问题,以及用以解决这些问题的语言...