`
61party
  • 浏览: 1055141 次
  • 性别: Icon_minigender_2
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

在C++中侦测内嵌型别的存在(rev#2)

阅读更多

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++中侦测内嵌型别的存在.doc

    在C++中侦测内嵌型别的存在 动机(Motivation) 假设一所大学的注册系统提供了一个注册函数: template void Register(T person) { Register(person,typename T::person_tag()); }; 而对于注册者有以下几种标识...

    Effective_Modern_C++_中文版

    想要彻底理解C++11和C++14,不可止步于熟悉它们引入的语言特性(例如,auto型别推导、移动语义、lambda表达式以及并发支持)。...“旧”C++程序设计(即C++98)中的最佳实践要求在现代C++的软件开发中作出哪些...

    QML与C++混合编程.pdf

    在C++实现中,非可视化的型别均为QObject的子类,可视化的类型均为QDeclarativeItem的子类。注意:QDeclarativeItem等同于QML的Item类。 如果用户想要定义自己的型别,做法如下: 在C++中,实现派生于QObject或...

    Effective Modern C++_英文原版_超高清_带书签_pdf

    想要彻底理解C++11和C++14,不可止步于熟悉它们引入的语言特性(例如,auto型别推导、移动语义、lambda表达式,以及并发支持)。挑战在于高效地运用这些特性,从而使你的软件具备正确性、高效率、可维护性和可移植性...

    More Effective C++

    条款34:如何在同一个程序中结合 C++ 和 C 270 Understand how to combine C++ and C in the same program 条款35:让自己习惯于标准 C++ 语言 277 Familiarize yourself with the language standard 推荐书目 285 ...

    零起点学通C++学习_多媒体范例教学代码

    12.1.2 继承和派生如何在C++中实现 12.1.3 继承的种类及语法 12.1.4 单一继承 12.2 公有型、保护型和私有型 12.3 访问权限 12.4 多重继承 12.5 继承的构造与析构 12.6 合理利用基类构造函数 12.7 继承和重载...

    零起点学通C++多媒体范例教学代码

    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...

    Essential C++英文版

    本书以4个面向来表现C++的本质:procedural(程序性的)、generic(泛型的)、object-based(个别对象的)、object-oriented(面向对象的)。本书的组织围绕着一系列逐渐繁复的程序问题,以及用以解决这些问题的语言...

    《概观C++程序设计语言》

    [§2 C++的设计和演化]: 介绍C++语言的设计目标和其演化发展的原则; [§3 C程序设计模型]: 介绍C++所包含的C语言子集以及其它支持传统的系统程序设计风格的语言设施; [§4 C++的抽象机制]: 介绍类(class...

    Iterator迭代器 所谓concept,是一组“描述某个型别”的条件。

    所谓concept,是一组“描述某个型别”的条件。当某个型别满足所有这样的条件,我们便说它是该concept的一个model。举例来说,char* 便是Input Iterator的model。

    Essential C++中文版(侯捷译)

    本书以4个面向来表现C++的本质:procedural(程序性的)、generic(泛型的)、object-based(个别对象的)、object-oriented(面向对象的)。本书的组织围绕着一系列逐渐繁复的程序问题,以及用以解决这些问题的语言...

    滤泡性辅助性T细胞在前列腺癌中对B细胞抗体型别转换的作用.docx

    滤泡性辅助性T细胞在前列腺癌中对B细胞抗体型别转换的作用.docx

    C/C++ 宏详解(详解)

    介绍了C/C++ 宏的用法和常用错误,众多C++书籍都忠告我们C语言宏是万恶之首,但事情总不如我们想象的那么坏,就如同goto一样。宏有 一个很大的作用,就是自动为我们产生代码。如果说模板可以为我们产生各种型别的...

    (C++经典名著,英文版)Effective.C.Plus.Plus

    例如,虽然我热切告诉你一些有关「撰写自己的operator new」的注意事项(条款7~10),但是我假设你可以从其他地方获知,operator new必须传回一个void*,其第一引数的型别必须是size_t。许多C++语言书可以带给你这样...

    Essential C++中文版

    本书以4个面向来表现C++的本质:procedural(程序性的)、generic(泛型的)、object-based(个别对象的)、object-oriented(面向对象的)。本书的组织围绕着一系列逐渐繁复的程序问题,以及用以解决这些问题的语言...

    C++类和接口的设计原则探讨

    设计一个高效率的类型(class 型别)

Global site tag (gtag.js) - Google Analytics