站在对象模型的尖端(On the Cusp of the Object Model)

Template

下面是有关template的三个主要讨论方向:

  1. template的声明,基本上来说就是当你声明一个template class、template class member function等等,会发生什么事情。
  2. 如何"具现(instantiates)"出class object以及inline nonmember,以及member template functions,这些是"每一个编译单元都会拥有的一份实体"的东西。
  3. 如何“具现”出nonmember以及member templates functions,以及static template class members,这些都是"每一个可执行文件中只需要一份实体"的东西,这也就是一般而言template所带来的问题。

Template的"具现"行为(Template Instantiation)

考虑下面的template Point class:

template<class Type>
class Point{
public:
enum Status { unallocated, normalized }; Point(Type x = 0.0, Type y = 0.0, Type z = 0.0);
~Point(); void *operator new(size_t );
void operator delete(void *, size_t );
//...
private:
static Point<Type> *freeList;
static int chunkSize;
Type _x, _y, _z;
};

首先,当编译器看到template class声明时,它会做出什么反应?在实际程序中,什么反应也没有!也就是说,上述的static data members并不可用。nested enum或其enumerators也一样。

虽然enum Status的真正类型在所有的Point instantiations中都一样,其enumerators也是,但它们每一个都只能通过template Point class的某个实体来存取或操作,因此我们可以这样写:

Point<float>::Status s;

但是不能这样写:

//error
Point::Status s;

同样的道理,freeList和chunkSize对程序而言也还不可用,我们不能够写:

//error
Point::freeList;

我们必须明确地指定类型,才能使用freeList:

Point<float>::freeList;

像上面这样使用static member,会使其一份实体与Point class的float instantiation在程序中产生关联,如果我们写:

//ok, 另一个实体(instance)
Point<double>::freeList;

就会出现第二个freeList实体,与Point class的double instantiation产生关联

一个class object的定义,不论是由编译器暗中地做,或是由程序员像下面这样明确地做:

const Point<float> origin;

都会导致template class的“具现”,也就是说,float instantiation的真正对象布局会被产生出来。

member functions(至少对于那些未被使用过的)不应该被“实体”化,只有在member functions被使用的时候,C++ Standard才要求它们被“具现”出来。当前的编译器并不精确遵循这项要求,之所以由使用者来主导“具现”规则,有两个主要原因:

  1. 空间和时间效率的考虑。如果class中有100个member functions,但你的程序只针对某个类型使用其中两个,针对另一个类型使用其中5个,那么其他193个函数都“具现”将花费大量的时间和空间。
  2. 尚未实现的功能,并不是一个template具现出来的所有类型就一定能够支持一组member functions所需要的所有运算符。如果只“具现”那些真正用到的memeber functions,template就能够支持那些原本可能会造成编译时期错误的类型(types)。

Template中的名称决议方式(Name Resolution within a Template)

你必须能够区分以下两种意义。一种是C++ Standard所谓的"Scope of the template definition",也就是“定义出template”的程序。另一种是C++ Standard所谓的"scope of the template instantiation",也就是说“具现出template”的程序。第一种情况举例如下:

//scope of the template definition
extern double foo(double); template<class type>
class ScopeRules{
public:
void invariant(){
_member = foo(val);
} type type_dependent(){
return foo(_member);
}
//...
private:
int _val;
type _member;
};

第二种情况举例如下:

//scope of the template instantiation
extern int foo(int);
//...
ScopeRultes<int> sr0;

在ScopeRules template中有两个foo()调用操作。在“scope of template definition”中,只有一个foo()函数声明位于scope之内。然而在“scope of template instantiation”中,两个foo()函数声明都位于scope之内。如果我们有一个函数调用操作:

//scope of the template instantiation
sr0.invariant();

那么,在invariant()中调用的究竟是哪一个foo()函数实体呢?

//调用的是哪一个foo()函数实体
_member = foo(_val);

在调用操作的那一点上,程序中的两个函数实体是:

//scope of the template declaration
extern double foo(double); //scope of the template instantiation
extern int foo(int);

而_val的类型是int,那么你认为选中的是哪一个呢?结果,被选中的是直觉以外的那一个:

//scope of the template declaration
extern double foo(double);

Template之中,对于一个nonmember name的决议结果是根据这个name的使用是否与“用以具现出该template的参数类型”有关而设定的。如果其使用互不相关,那么就以“scope of the template declaration”来决定name。如果其使用互有关联,那么就以“scope of template instantiation”来决定name。在第一个例子中,foo()与用以具现ScopeRules的参数类型无关:

//the resolution of foo() is not
//dependent on the template argument
_member = foo(val);

这是因为_val的类型是int, _val是一个“类型不会变动”的template class member。也就是说,被用来具现出这个template的真正类型,对于 _val的类型并没有影响。此外,函数的决议结果只和函数的原型(signature)有关,和函数的返回值没有关联。因此, _

member的类型并不会影响哪一个foo()实体被选中。foo()的调用与template参数毫无关联! 所以调用操作必须根据"scope of the template declaration"来决议。在此scope中,只有一个foo()候选者。

让我们另外看看"与类型相关"(type-dependent)的用法:

sr0.type_dependent();

这个函数的内容如下:

return foo(_member);

它究竟会调用哪一个foo()呢?

这个例子很清楚地与template参数有关,因为该参数将决定_member得真正类型。所以,这一次foo()必须在"scope of the template instantiation"中决议,本例中这个scope有两个foo()函数声明。由于 _member的类型在本例中为int,所以应该是int版的foo()出线。如果ScopeRules是以unsigned int或long类型具现出来,那么foo()调用操作就暧昧不明。最后,如果ScopeRules是以某一个class类型具现出来,而该class没有针对int或double实现出convertion运算符,那么foo()调用操作会被标识为错误。不管如何改变,都是由"scope of the template instantiation"来决定,而不是由"scope of the template declaration"决定。

这意味着一个编译器必须保持两个scope contexts:

  1. “Scope of the template declaration”,用以专注于一般的template class
  2. "Scope of the template instantiation", 用以专注于特定的实体

编译器的决议(resolution)算法必须决定哪一个才是适当的scope,然后在其中搜寻适当的name。

Member Function的实例化行为(Member function instantiation)

对于template的支持,最困难莫过于template function的具现(instantiation),目前的编译器提供了两个策略:一个是编译时期策略,程序代码必须在program text file中备妥可用;另一个是链接时期策略,程序代码必须在meta-compliation工具可以导引编译器的具现行为(instantiation)。

下面是编译器设计者必须回答的三个主要问题:

  1. 编译器如何找出函数的定义?

    答案之一是包含template program text file,就好像它是个header文件一样,Borland编译器就是遵循这个策略。另一种方法是要求一个文件命名规则,例如,我们可以要求,在Point.h文件中发现的函数声明,其template program text一定要放置于文件Point.c或者Point.cpp中,以此类推。cfront就是遵循这个策略。Edison Desigin Group编译器对此两种策略都支持。
  2. 编译器如何能够只具现出程序中用到的member functions?

    解决办法之一就是,根本忽略这项要求,把一个已经具现出来的class的所有member functions都产生出来。Borland就是这么做的——虽然它也提供#pragmas让你压制(或具现出)特定实体。另一种策略就是仿真链接操作,检测看看哪一个函数真正需要,然后只为它(们)产生实体。cfront就是这么做的,Edison Design Group编译器对此两种策略都支持。
  3. 编译器如何阻止member definitions在多个 .o文件中都被具现呢?

    解决办法之一是产生多个实体,然后从链接器中提供支持,只留下其中一个实体,其余都忽略。另外一个办法就是由使用者来导引“仿真链接阶段”的具现策略,决定哪些实体(instances)才是所需求的。

目前,不论是编译时期还是链接时期的实例化(instantiation)策略,均存在以下弱点:当template实例被产生出来时,有时候会大量增加编译时间。很显然,这将是template functions第一次实例化时的必要条件。然而当那些函数被非必要地再次实例化,或是当“决定那些函数是否需要再实例化”所花的代价太大时,编译器的表现令人失望

C++支持template的原始意图可以想见是一个由使用者导引的自动实例化机制,既不需要使用者的介入,也不需要相同文件有多次的实例化行为。但是这已被证明是非常难以达成的任务,比任何人此刻所想象的还要难。

异常处理(Exception Handing)

欲支持exception handling,编译器的主要工作就是找出catch子句,以处理被丢出来的exception。这多少需要追踪程序堆栈中的每一个函数当前作用区域(包括追踪函数中的local class objects当时的情况)。同时,编译器必须提供某种查询exception objects的方法,以知道其实际类型(这直接导致某种形式的执行期识别,也就是RTTI)。最后,还需要某种机制用以管理被丢出的object,包括它的产生、储存、可能的解构(如果有相关的destructor)、清理(clean up)以及一般存取,也可能有一个以上的objects同时起作用。

一般而言,exception handling机制需要与编译器所产生的数据结构以及执行期的一个exception library紧密合作,在程序大小和执行速度之间,编译器必须有所抉择:

  • 为了维持执行速度,编译器可以在编译时期建立起用于支持的数据结构,这会使程序大小膨胀,但编译器可以几乎忽略这些结构,直到有个exception被丢出来。
  • 为了维持程序大小,编译器可以在执行期建立起用于支持的数据结构。这会影响程序的执行速度,但意味着编译器只有在必要的时候才建立那些数据结构(并且可以抛弃之)。

Exception Handling 快速检阅

C++的exception handing由三个主要的语汇组件构成:

  1. 一个throw子句。它在程序某处发出一个exception。被抛出去的expection可以是內建类型,也可以是使用者自定类型。
  2. 一个或多个catch子句。每一个catch子句都是一个exception handler。它用来表示说,这个子句准备处理某种类型的exception,并且在封闭的大括号区段中提供实际的处理程序
  3. 一个try区段。它被围绕以一系列的叙述句(statements),这些叙述句可能会引发catch子句起作用

当一个exception被丢出去时,控制权会从函数调用中被释放出来,并寻找一个吻合的catch子句。如果都没有吻合者,那么默认的处理例程terminate()会被调用。当控制权被抛弃后,堆栈中的每一个函数调用也就被推离(popped up),这个程序称为unwinding the stack。在每一个函数被推离堆栈之前,函数的local class objects的destructor会被调用。

对Exception Handling的支持

当一个exception发生时,编译系统必须完成以下事情:

  1. 检验发生throw操作的函数;
  2. 决定throw操场是否发生在try区段中;
  3. 若是,编译系统必须把exception type拿来和每一个catch子句比较;
  4. 如果比较吻合,流程控制应该交到catch子句手中;
  5. 如果throw的发生并不在try区段中,并没有一个catch子句吻合,那么系统必须(a)摧毁所有active local objects,(b)从堆栈中将当前的函数"unwind"掉,(c)进行到程序堆栈中的下一个函数中去,然后重复上述步骤2~5

当一个实际对象在程序执行时被丢出,会发生什么事?

当一个exception被丢出时,exception object会被产生出来并通常放置在相同形式的exception数据堆栈中,从throw端传染给catch子句的是exception object的地址、类型描述器(或是一个函数指针,该函数会传回与该exception type有关的类型描述器对象),以及可能会有的exception object描述器(如果有人定义它的话)。

考虑一个catch子句如下:

catch(exPoint p){
//do something
throw;
}

以及一个exception object,类型为exVertex,派生自exPoint。这两种类型都吻合,于是catch子句会作用起来。那么p会发生什么事?

  • p将以exception object作为初值,就像是一个函数参数一样。这意味着如果定义有(或由编译器合成出)一个copy constructor和一个destructor的话,它们都会实施于local copy身上。
  • 由于p是一个object而不是一个reference,当其内容被拷贝的时候,这个exception object的non-exPoint部分会被切掉(sliced off)。此外,如果为了exception的继承而提供有virtual functions,那么p的vptr会被设为exPoint的virtual table;exception object的vptr不会被拷贝。

当这个exception被再丢出一次时,会发生什么事情呢?p是一个local object,在catch子句的末端将被摧毁。丢出p需得产生另一个临时对象,并意味着丧失原来的exception的exVertex部分。原来的exception object被再一次丢出,任何对p的修改都会被抛弃。

像下面这样的一个catch子句:

catch(exPoint &rp){
//do something
throw;
}

则是参考到真正的exception object。任何虚拟调用都会被决议(resolved)为instances active for exVertex,也就是exception object的真正类型。任何对此object的改变都会被复制到下一个catch子句中。

执行期类型识别(Runtime Type Identification, RTTI)

在cfront中,用以表现出一个程序所谓的“内部类型体系”,看起来像:

//程序层次结构的根类 root class
class node{ ... }; //root of 'type' subtree: basic types,
//'derived' types: points, arrays,
//functions, classes, enums, ...
class type : public node{ ... }; //two representations for functions
class fct : public type{ ... };
class gen : public type{ ... };

其中gen是generic的简写,用来表现一个overloaded function。

于是只要你有一个变量,或是类型为type*的成员(并知道它代表一个函数),你就必须决定其特定的derived type是否为fct或是gen。

Type-Safe Downcast(保证安全的向下转型操作)

一个type-safe downcast(保证安全地向下转换操作)必须在执行期对指针有所查询,看看它是否指向它所展现(表达)之object的真正类型。因此,欲支持type-safe downcast在object空间和执行时间上都需要一些额外的负担:

  • 需要额外的空间以存储类型信息(type information),通常是一个指针,指向某个类型信息节点
  • 需要额外的时间以决定执行期的类型(runtime type),因为,正如其名所示,这需要再执行期才能决定

冲突发生在两组使用者之间:

  1. 程序员大量使用多态(polymorphism),并因而需要正统而合法的大量downcast操作。
  2. 程序员使用内建数据类型以及非多态设备,因而不受各种额外负担所带来的报应。

理想的解决方案是:为两派使用者提供正统而合法的需要——虽然或许得牺牲一些设计上的纯度与优雅性。

C++的RTTI机制提供一个安全的downcast设备,但只对那些展现"多态(也就是使用继承和动态绑定)"的类型有效。我们如何分辨这些?编译器能否光看class的定义就决定这个class用以表现一个独立的ADT或是一个支持多态的可继承子类型(subtype)?当然,策略之一就是导入一个新的关键词,优点是可以清楚地识别出支持新特性的类型,缺点则是必须翻新旧程序。

另一个策略是经由声明一个或多个virtual functions来区别class声明。其优点是透明化地将旧有程序转化过来,只要重新编译就好。缺点则是可能会将一个其实并非必要的virtual function强迫导入继承体系的base class身上。在C++中,一个具备多态性质的class(所谓的polymorphic class),正是内含继承而来(或是直接声明)的virtual functions。

从编译器的角度来看,这个策略还有其他优点,就是大量降低额外负担。所有polymorphic classes的objects都维护了一个指针(vptr),指向virtual function table,只要我们把与该class相关的RTTI object地址放进virtual table中(通常放在第一个slot),那么额外负担就降低为:每一个class object只多花费一个指针。这个指针只需被设定一次,它是被编译器静态设定,而不是在执行期由class constructor设定(vptr才是这么设定)。

Type-Safe Dynamic cast(保证安全的动态转型)

dynamic_cast运算符可以在执行期决定真正的类型。如果downcast是安全的(也就是说,如果base type pointer指向一个derived class object),这个运算符会传回被适当转换过的指针。如果downcast不是安全地,这个运算符会传回0

References并不是Pointers

程序中对一个class指针类型施以dynamic_cast运算符,会获得true或false:

  • 如果传回真正的地址,表示这个object的动态类型被确认了,一些与类型相关的操作现在可以施行于其上。
  • 如果传回0,表示没有指向任何object,意味应该以另一种逻辑施行于这个动态类型未确定的object身上。

dynamic_cast运算符也适用于reference身上。然而对于一个non-type-safe cast,其结果不会与施行于指针的情况相同。为什么?一个reference不可以像指针那样"把自己设为0就代表了 no object";若将一个reference设为0,会引起一个临时性对象(拥有被参考到的类型)被产生出来,该临时对象的初值为0,这个reference然后被设定成为该临时性对象的一个别名(alias)。

因此当dynamic_cast运算符施行于一个reference时,不能够提供对等于指针情况下的那一组true/false。取而代之的是,会发生下列事情:

  • 如果reference真正参考到适当的derived class(包括下一层或下下一层或下下下一层或...),downcast会被执行而程序可以继续执行。
  • 如果reference并不真正是某一种derived class,那么,由于不能传回0,遂丢出一个bad_cast exception.

Typeid运算符

typeid运算符传回一个const reference,类型为type_info。

type_info object由什么组成? C++ Standard中对type_info的定义如下:

class type_info{
public:
virtual ~type_info();
bool operator==(const type_info& ) const;
bool operator!=(const type_info& ) const;
bool before(const type_info&) const;
bool char* name() const; //传回class原始名称
private:
//prevent memberwise init and copy
type_info(const type_info& );
type_info& operator=(const type_info& );
//data members
};

编译器必须提供的最小量信息是class的真实名称、以及在type_info objects之间的某些排序算法(这就是before()函数目的)、以及某些形式的描述器,用以表现explicit class type和这个class的任何subtype。

虽然RTTI提供的type_info对于exception handling的支持来说是必要的,但对于exception handling的完整支持而言,还不够。如果再加上额外一些type_info derived classes,就可以在exception发生时提供有关于指针、函数及类等等的更详细信息。

深入探索C++对象模型(七)的更多相关文章

  1. 读书笔记《深度探索c++对象模型》 概述

    <深度探索c++对象模型>这本书是我工作一段时间后想更深入了解C++的底层实现知识,如内存布局.模型.内存大小.继承.虚函数表等而阅读的:此外在很多面试或者工作中,对底层的知识的足够了解也 ...

  2. 柔性数组-读《深度探索C++对象模型》有感 (转载)

    最近在看<深度探索C++对象模型>,对于Struct的用法中,发现有一些地方值得我们借鉴的地方,特此和大家分享一下,此间内容包含了网上搜集的一些资料,同时感谢提供这些信息的作者. 原文如下 ...

  3. 柔性数组-读《深度探索C++对象模型》有感

    最近在看<深度探索C++对象模型>,对于Struct的用法中,发现有一些地方值得我们借鉴的地方,特此和大家分享一下,此间内容包含了网上搜集的一些资料,同时感谢提供这些信息的作者. 原文如下 ...

  4. [读书系列] 深度探索C++对象模型 初读

    2012年底-2014年初这段时间主要用C++做手游开发,时隔3年,重新拿起<深度探索C++对象模型>这本书,感觉生疏了很多,如果按前阵子的生疏度来说,现在不借助Visual Studio ...

  5. 拾遗与填坑《深度探索C++对象模型》3.3节

    <深度探索C++对象模型>是一本好书,该书作者也是<C++ Primer>的作者,一位绝对的C++大师.诚然该书中也有多多少少的错误一直为人所诟病,但这仍然不妨碍称其为一本好书 ...

  6. 拾遗与填坑《深度探索C++对象模型》3.2节

    <深度探索C++对象模型>是一本好书,该书作者也是<C++ Primer>的作者,一位绝对的C++大师.诚然该书中也有多多少少的错误一直为人所诟病,但这仍然不妨碍称其为一本好书 ...

  7. 深度探索C++对象模型

    深度探索C++对象模型 什么是C++对象模型: 语言中直接支持面向对象程序设计的部分. 对于各个支持的底层实现机制. 抽象性与实际性之间找出平衡点, 需要知识, 经验以及许多思考. 导读 这本书是C+ ...

  8. 《深度探索C++对象模型》读书笔记(一)

    前言 今年中下旬就要找工作了,我计划从现在就开始准备一些面试中会问到的基础知识,包括C++.操作系统.计算机网络.算法和数据结构等.C++就先从这本<深度探索C++对象模型>开始.不同于& ...

  9. C++的黑科技(深入探索C++对象模型)

    周二面了腾讯,之前只投了TST内推,貌似就是TST面试了 其中有一个问题,“如何产生一个不能被继承的类”,这道题我反反复复只想到,将父类的构造函数私有,让子类不能调用,最后归结出一个单例模式,但面试官 ...

随机推荐

  1. myeclipse生成类的帮助文档

    http://blog.csdn.net/tabactivity/article/details/11807233

  2. 第13章 学习shell script

    由于博客园中dollar符号有别的意义,所以文中的dollar符号使用¥表示 第一个script [root@localhost script]# cat -n sh01.sh #!/bin/bash ...

  3. FTP渗透测试

    在这篇文章中,我们将学习如何在CentOS机器配置ProFTPD的服务.之后,我们将进行渗透测试,以评估FTP服务的安全性,那么我们也将学习漏洞的对策. 在CentOS Linux机器的安装和配置FT ...

  4. 使用ssh公钥登陆

    记录一下使用的具体命令,具体参考: Centos设置禁止密码登录而只使用密钥登录SSH方法  优先参考这个. ssh使用公钥授权不通过的问题解决 Xshell配置ssh免密码登录-密钥公钥(Publi ...

  5. 【week11】回顾

    一.回答五个问题 第一次阅读<构建之法>之后的五个问题: 1.关于敏捷,书中说了我理解的就是介绍了敏捷就是“没有既定的计划与文档,马上写代码,随时发牢骚”,但是开发也是需要有一定的流程的, ...

  6. 【beta】Scrum站立会议第5次....11.7

    小组名称:nice! 组长:李权 成员:于淼  刘芳芳韩媛媛 宫丽君 项目内容:约跑app(约吧) 时间:2016.11.7   12:00——12:30 地点:传媒西楼220室 本次对beta阶段的 ...

  7. Linux下修改环境变量PATH

    1.什么是环境变量(PATH) 在Linux中,在执行命令时,系统会按照PATH的设置,去每个PATH定义的路径下搜索执行文件,先搜索到的文件先执行. 我们知道查阅文件属性的指令ls 完整文件名为:/ ...

  8. OpenGL 加载DDS文件(压缩纹理)

    想必很多人都见过DDS这种文件,它是一个“图片文件”,如果你安装了某些看图软件,你可以直接双击打开它来进行预览. 那么,这种DDS文件和我们常见的TGA/PNG之类的文件有何不同呢? DDS和TGA/ ...

  9. CSS预处理语言-less 的使用

    Less 是一门 CSS 预处理语言,它扩展了 CSS 语言,增加了变量.Mixin.函数等特性,使 CSS 更易维护和扩展. Less 可以运行在 Node 或浏览器端. Less的编译处理 作为一 ...

  10. P3916 图的遍历

    题目描述 给出 NNN 个点, MMM 条边的有向图,对于每个点 vvv ,求 A(v)A(v)A(v) 表示从点 vvv 出发,能到达的编号最大的点. 输入输出格式 输入格式: 第1 行,2 个整数 ...