在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的对象为学生类对象时,typenameT::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, typenameX::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{

template<class>

struct key_type{};

};

template<class T>

struct does_typedef_exists

{

typedef does_sometypedef_exists<T> impl_type;

static const bool value = impl_type::value;

};

int main()

{

std::cout << does_typedef_exists<A>::value<<' '

<< does_typedef_exists<B>::value<<' '

<< does_typedef_exists<C>::value<<' '

<< does_typedef_exists<D>::value<<' '

<< does_typedef_exists<E>::value<<' '

<< std::endl;

return 0;

};

在我的VC7.0编译平台上:

如果使用第一种实现,这将输出:0 1 0 0 1

如果使用第二种实现,这将输出:0 1 0 0 0

很显然,两种实现对于struct E给出的结果不一样。事实上,我们希望该traits对E这种情况给出的结果为1。从这一点讲第一种实现在我的编译器上已经神差鬼使的成功了,而第二种实现还没有。不管怎样,我们都必须试图找到一种方法来实现它。这种方法不可以像实现一那样依赖与编译器的可能的“一时糊涂”,它应该是以C++标准的规则为依据的。Paul Mensonides提供了一种方法,然而在我的VC7.0上编译不能通过。后面我会介绍它。

改进(Improvement)

第一种实现还可以做一点改进,像这样:

template<class T>

struct does_sometypedef_exists

{

template<class U>

static yes_type check(typename U::key_type* );

template<class U>

static no_type check(...);

static const bool value = sizeof(check<T>(0))==sizeof(yes_type);

};

这样,去掉static T t,和check的第一个参数,会使代码看上去更简洁和更可靠一些。

封装(Encapsulation)

现在我们的traits只能侦测typename T::key_type的存在性,我们需要一个扩充的机制,以让我们能够侦测任意名称的内嵌类型的存在性。我们使用宏:

#define IMPLEMENT_TYPEDEF_EXISTS(id) /

template<class T> /

struct does_sometypedef_exists_##id /

{ /

private: /

template<class U> /

static yes_type check(typename U::id*); /

template<class U> /

static no_type check(...); /

public: /

static const bool value=sizeof(check<T>(0))==sizeof(yes_type); /

};

#define DOES_TYPEDEF_EXISTS(T,id) /

does_sometypedef_exists_##id<T>

经过这重封装,当你要侦测某个名称的内嵌类型如some_type时,你先在任何函数之外写这样的代码:

IMPLEMENT_TYPEDEF_EXISTS(some_type)

这将会扩展成一个名为does_sometypedef_exists_some_type的模板类,然后你这样使用它:

DOES_TYPEDEF_EXISTS(X,some_type)::value;

这将侦测类X中有没有some_type。不将::value直接纳入到宏中的原因是为了保留traits编程的风格。

Paul Mensonides对内嵌template的侦测方法

Paul Mensonides是Boost库的preprocesser部分的设计者,那完全是一个宏的世界,也是Boost库中的一个十分精巧的部分。我最初是在comp.lang.c++.moderated上看到他关于这个问题的解答的。

template<class> struct split;  // 缺省声明,因为不会被匹配所以不用定义

// 以下是偏特化

template< template<class> class T, class T1 > // T为模板

struct split< T<T1> > {

struct type { };

};

template< template<class, class> class T, class T1, class T2 >

struct split< T<T1, T2> > {

struct type { };

};

// etc. :(,后面有支持更多模板参数的版本,从略

template<class T> class has_template_key_type

{

private:

template<class U>

static yes_type check(

typename split<

typename U::template key_type<null_t> >::type*

);

template<class U>

static yes_type check(

typename split<

typename U::template key_type<null_t, null_t> >::type*

);

// etc. :( 后面有支持更多模板参数的版本,从略

template<class U> static no_type check(...);

public:

static const bool value

= sizeof(check<T>(0)) == sizeof(yes_type);

};

template<class T, bool V = has_template_key_type<T>::value>

class has_key_type

{

private:

template<class U> static yes_type check(typename U::key_type*);

template<class U> static no_type check(...);

public:

static const bool value

= sizeof(check<T>(0)) == sizeof(yes_type);

};

template<class T> struct has_key_type<T, true>

{

static const bool value = false;

};

Paul Mensonides说它能够工作,我也觉得根据标准它也该能够工作,但事实是在我的VC7.0上编译器有一大堆抱怨。我试了其它各种方法,结果总是类似的编译错误将我挡住。我希望它在你的编译器上能够工作。

这里的原理是这样的,如果类型X有内嵌模板类型定义key_type,则has_template_key_type中的返回yes_type的那些成员函数总有一个能够与它匹配,而其它则不会被实例化(VC7.0仿佛总试图将其它的也实例化了,结果它总会抱怨说模板参数太少或太多)。

然而Paul Mensonides的这个解决方案还有个问题:如果那个内嵌的模板类的定义像如下这个样子:

template<int>

struct key_type{};

则将没有任何一个返回yes_type的重载版本能和它匹配,看看split类的定义吧,它的template template模板参数的形式是template<class[ ,class ,...]> class T,而上面的key_type的形式为template<int> class key_type,它们无法匹配,如果试图再加入一个能与其匹配的split偏特化版本:

template<template<int>class T,int T1> struct split<T<T1> >{...};

这也是不实际的。因为int和class可能有无穷多种组合。如果key_type再变成template<int, class> class key_type呢?如果...,总之,如你所见,以int这类non-type parameter作为模板参数的加入使事情有了无限多种可能。split将穷于应付。

结论(Conclusion)

对于最后我提出的问题,仿佛没有一个好的解决方案。所以只能放弃这种内嵌template的可能,假定情况是单纯的。对于后者,这种技术有较好的表现。

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

  1. GCC中的内嵌汇编语言

    一.声明   虽然Linux的核心代码大部分是用C语言编写的,但是不可避免的其中还是有一部分是用汇编语言写成的.有些汇编语言代码是直接写在汇编源程序中的,特别是Linux的启动代码部分:还有一些则是利 ...

  2. gcc中的内嵌汇编语言(Intel i386平台)

    gcc中的内嵌汇编语言(Inteli386平台) 一.声明 虽然Linux的核心代码大部分是用C语言编写的,但是不可避免的其中还是有一部分是用汇编语言写成的.有些汇编语言代码是直接写在汇编源程序中的, ...

  3. html内嵌框架怎么写,在HTML网页中,内嵌框架的标签代码是()。

    在HTML网页中,内嵌框架的标签代码是(). 更多相关问题 每年人环奖的考试报名时间大约在5月份 嗅觉的香气包裹在闻气味的人周围,成为一种氛围,构成( )天空. Access的控件对象可以设置某个属性 ...

  4. 在HTML中 表示内嵌CSS样式的标记,html怎么加css样式

    html怎么加css样式?下面本篇文章就来给大家介绍一下在HTML中添加CSS样式的方法.有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助. 1.行内式(使用style属性,在HTML标 ...

  5. linux smp lock 锁总线 内嵌汇编,gcc中的内嵌汇编语言(Inteli386平台)

    gcc中的内嵌汇编语言(Intel i386平台) 一.声明 虽然Linux的核心代码大部分是用C语言编写的,但是不可避免的其中 还是有一部分是用汇编语言写成的.有些汇编语言代码是直接写在汇 编源程序 ...

  6. 邮件中html内嵌图片,邮件内嵌html

    一般的HTML格式电子邮件,都可以使用内建有HTML编辑器的电子邮件程序(如:Outlook Express)编写发送.我们以Outlook Express 5.0(简称OE 5.0)为例,点击工具栏 ...

  7. 使用POI在ppt中加入内嵌excel

    基本实现了该功能,但打开内嵌excel会有点问题,很期待大佬的解决方案 public class EmbedExcel {private static InputStream embedPicture ...

  8. 邮件中html内嵌图片,在html电子邮件中嵌入图像

    尝试直接插入,这样您就可以在电子邮件的不同位置插入多个图像. 并且要使这篇文章对其他人有用:如果你没有base64数据字符串,可以从图像文件中轻松地创建一个:http: //www.motobit.c ...

  9. 邮件中html内嵌图片,email - 在HTML电子邮件中嵌入附加图像 - SO中文参考 - www.soinside.com...

    你的问题的答案是在春季文档here. mailSender.send(new MimeMessagePreparator() { public void prepare(MimeMessage mim ...

最新文章

  1. J钓鱼 (vector + queue)
  2. 记录一次生产发布事件——(简单的非空验证也能引发大问题)
  3. 免费发布一个简单而有趣的计算工具
  4. Spring学习(三)Spring 整合MyBatis、声明式事务
  5. 锐起无盘服务器陈列设置,原创]锐起无盘安装全图文设置,含SCII设置在内
  6. 用spss进行数据的标准化处理_spss 如何进行数据标准化_spss原始数据标准化_spss数据标准化处理 如何使用SPSS做时间序列分析_spss时间序列分析...
  7. 华为首度公开披露员工薪酬:去年平均年薪近28万
  8. 计算机漏洞安全相关的概念POC 、EXP 、VUL 、CVE 、0DAY
  9. 京东咚咚架构演讲读后感
  10. 郑州计算机安全协会安全员考试成绩,安全员C证考试结果查询
  11. 裸金属服务器启动之PXE与IPXE实践
  12. 按关键字采集淘特商品列表API接口H5
  13. SAP学习笔记(SD的Delivery) 2008-12-28
  14. CV笔记1:颜色空间介绍及转换
  15. 2022年5个人工智能趋势
  16. 西安电子科技大学c语言答案,西安电子科技大学2021春 面向对象程序设计(C++)大作业答案...
  17. 项目经理start法则_开放科学项目的7条经验法则
  18. 八种必备户外装备包你踏遍地球
  19. 用机器学习实现情感分析
  20. 怎么在通达信上设置连板次数以及所属行业

热门文章

  1. 简说创业公司的技术选型,从BearyChat的开发说起
  2. Java Review - 并发编程_LinkedBlockingQueue原理源码剖析
  3. 实战SSM_O2O商铺_42【前端展示】店铺列表页面View层的实现
  4. 实战SSM_O2O商铺_28【商品】商品添加之Dao层的实现
  5. 使用uni-app开发微信小程序之登录模块
  6. 基于多线程队列简单实现mq
  7. Redis之字典(hashtable)
  8. 服务器定期巡检项目,服务器定期巡检制度..docx
  9. 分页请求json数据_pyspider抓取虎嗅网文章数据
  10. 2021-01-21 linux shell脚本实现间隔一定时间执行一次任务