深度探索C++对象模型(七)站在对象模型的尖端

如果我们定义一个指针,指向特定的实例,像这样:

Point<float> *ptr=0;

程序中什么也没有发生。为什么呢?因为一个指向class object的指针,本身并不是一个class object,编译器不需要知道与该class有关的任何members的数据或object布局数据。所以将“Point的一个float实例”实例化也就没有必要了。在C++ Standard完成之前,“声明一个指针指向某个template class”这件事情并未被强制定义,编译器可以自行解决要或不要将template“实例化”。cfront就是这么做的(这使得某些程序员大感困窘)!如今C++ Standard已经禁止编译器这么做。

如果不是pointer而是reference,又如何?假设:

const Point<float> &ref = 0;

是的,它真的会实例化一个“Point的float实例”。这个定义的真正语意会被扩展为:

//内部扩展
Point<float> temporary(float (0));
const Point<float> &ref=temporary;

为什么?因为reference并不是无物(no object)的代名词。0被视为整数,必须被转换为以下类型的一个对象:

Point<float>

如果没有转换的可能,这个定义就是错误的,会在编译时被挑出来。

所以,一个class object的定义,不论是由编译器暗中地做(像稍早程序代码中出现过的temporary),或是由程序员像下面这样显示地做:

const Point<float> origin;

都会导致template class的“实例化”,也就是说,float instantiation的真正对象布局会被产生出来。回顾先前的template声明,我们看到Point由三个nonstatic members,每一个的类型都是Type。Type现在被绑定为float,所以origin的配置空间必须足够容纳三个float成员。

然而,member functions(至少对于那些未被使用过的)不应该被“实例化”。只有在member functions被使用的时候,C++ Standard才要求它们被“实例化”。目前的编译器并不精确遵守这项要求。之所以由使用者来主导“实例化”规则,由两个主要原因:
1、空间和时间效率的考虑。如果class中有100个member functions,但你的程序只针对某个类型使用其中两个,针对另一个类型使用其中五个,那么将其他193个函数都“实例化”将会花费大量的时间和空间。
2、尚未实现的机能。并不是一个template实例化的所有类型就一定能够完整支持一组member functions所需要的所有运算符。如果只“实例化”那些真正用到的member functions,template就能够支持那些原本可能会造成编译时期错误的类型(types)。

举个例子,origin的定义需要调用Point的default constructor和destructor,那么只有这两个函数需要被“实例化”。类似的道理,当程序员写:

Point<float> *p = new Point<float>;

时,只有(1)Point template的float实例、(2)new运算符、(3)default constructor需要被“实例化”。有趣的是,虽然new运算符是这个class的一个implicity static member,以至于它不能够直接处理其任何一个nonstatic members,但它还是依赖真正的template参数类型,因为它的第一个参数size_t代表class的大小。

这些函数在什么时候“实例化”?目前流行两种策略:
1、在编译的时候。那么函数将“实例化”于origin和p存在的那个文件中。
2、在链接的时候。那么编译器会被一些辅助工具重新激活。template函数实例可能被放在这一文件中、别的文件中或一个分离的存储位置。

Template的错误报告(Error Reporting within a Template)
在template class中,所有与类型有关的检验,如果牵涉到template参数,都必须延迟到真正的实例化操作(instantiation)发生,才得为之。
那么,什么样的错误会在编译器处理template声明时被标示出来?这有一部分和template的处理策略有关。cfront对template的处理是完全解析(parse)但不做类型检验;只有在每一个实例化操作(instantiation)发生时才做类型检验。所以在一个parsing策略之下,所有词汇(lexing)错误和解析(parsing)错误都会处理template声明的过程中标示出来。

在一个十分普遍的替代策略中,template的声明被收集成为一系列的“lexical tokens”,而parsing操作延迟直到真正有实例化操作(instantiation)发生时才开始。每当看到一个instantiation发生,这组token就被推往parser,然后调用类型检验,等等。

目前的编译器,面对一个template声明,在它被一组实际参数实例化之前,只能施行以有限的错误检查。template中那些与语法无关的错误,程序员可能认为十分明显,编译器却让它通过了,只有在特定实例被定义之后,才会发出抱怨。这是目前实现技术上的一个大问题。

Nonmember和member template functions在实例化行为(instantiation)发生之前也一样没有做到完全的类型检验。这导致某些十分露骨的template错误声明竟然得以通过编译。当然,这是编译器设计者自己决定的。Template facility并没有说不允许对template声明的类型部分有更严酷的检验。当然,像这样的错误是可以被发现并标示出来的。只不过是没有人去做罢了。

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);
//...
ScopeRules<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)

在上面程序中被选中的是:

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

Template之中,对于一个nonmember name的决议结果,是根据这个name的使用是否与“用以实例化该template的参数类型”有关而决定的。如果其使用互不相关,那么就以“scope of the template declaration”来决定name。如果其使用互有关联,那么就以“scope of the 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的类型并没有影响。此外,函数的决议结果只和函数的原型有关,和函数的返回值没有关系。因此_member的类型并不会影响哪一个foo()实例被选中。foo()的调用与template参数毫无关系!所以调用操作必须根据“scope of the template declaration”来决议。在此scope中,只有一个foo()候选者。

编译器必须保持两个scope contexts:
1、“scope of the template declaration”,用以专注于一般的template class。
2、“scope of the template instantiation”,用以专注于特定的实例。

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

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

C++支持template的原始意图可以想见一个由使用者导引的自动实例化机制,既不需要使用者的介入,也不需要相同文件有多次的实例化行为。但是这已被证明是非常难以达成的任务,比任何人此刻所能想象的还要难。
Edison Design Group开发出一套第二代的directed-instantiation机制,非常接近(我所知的)template facility原始含义。它主要运作如下:
1、一个程序的原始码被编译时,最初并不会产生任何“template实例化”。然而,相关信息已经被产生于object files之中。
2、当object files被链接在一块儿时,会有一个prelink程序被执行起来。它会检查object files,寻找template实例的相互参考以及对应的定义。
3、对于每一个“参考到template实例”而“该实例却没有定义”的情况,prelink将该文件视为与另一个实例化等同。以这种方法,就可以将必要的程序实例化操作指定给特定的文件。这些都会注册在prelinker所产生的.ii文件中(放在磁盘目录ii_file)。
4、prelinker重新执行编译器,重新编译每一个“.ii文件曾被改变过”的文件。这个过程不断重复,直到所有必要的实例化操作都已完成。
5、所有的object files被链接成一个可执行文件。

(二)异常处理(Exception Handling)
一般而言,exception handling机制需要与编译器所产生的数据结构以及执行期的一个exception library紧密合作。在程序大小和执行速度之间,编译器必须有所抉择:
1、为了维护执行速度,编译器可以在编译时期建立起用于支持的数据结构。这会使程序的大小发生膨胀,但编译器可以几乎忽略这些结构,直到有个exception被抛出来。
2、为了维护程序大小,编译器可以在执行期建立起用于支持的数据结构。这会影响程序的执行速度,但意味着编译器只有在必要的时候才建立那些数据结构(并且可以抛弃之)。

Exception Handling快速检阅
C++的exception handling由三个主要的语汇组件构成:
1、一个throw子句。它在程序某处发出一个exception。被抛出去的exception可以是内建类型,也可以是使用者自定类型。
2、一个或多个catch子句。每一个catch子句都是一个exception handler。它用来表示说,这个子句准备处理某种类型的exception,并且在封闭的大括号区段中提供实际的处理程序。
3、一个try区段。它被围绕以一系列的叙述句,这些叙述句可能会引发catch子句起作用。

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 《深度探索C++对象模型》是由侯捷所著的一本经典的C++图书,该书于2012年由机械工业出版社出版。本书的主要内容涵盖了C++对象模型的深入解析和探讨。 在书中,作者详细讲解了C++中的对象模型和相关的概念,如类、对象、继承、多态等。作者首先介绍了C++对象模型的基本概念和特点,包括对象的内存布局、虚函数表和虚函数指针等。然后,作者深入探讨了C++中的继承机制和多态性,包括单继承、多继承、虚继承等。作者还详细介绍了虚函数的实现原理和使用方法。 在书中,作者对C++对象模型的实现细节进行了深入的剖析,包括成员变量和成员函数的内存布局、函数指针和成员函数指针的用法等。同时,作者还讨论了C++中的一些高级特性,如模板、内存管理和异常处理等。通过对C++对象模型深度探索,读者可以更好地理解C++的内部机制和原理,提高程序设计和开发能力。 《深度探索C++对象模型》适合具有一定的C++编程基础的读者阅读,尤其是对C++对象模型感兴趣的读者。通过阅读本书,读者可以进一步了解C++的底层实现和运行机制,从而提高自己的编程能力和代码质量。此外,本书还提供了大量的示例代码和实践案例,可以帮助读者更好地理解和应用所学知识。 总之,《深度探索C++对象模型》是一本深入探讨C++对象模型的经典著作,通过对C++的底层实现和内部机制的剖析,帮助读者深入理解C++编程语言,并提高自己的软件开发能力。 ### 回答2: 《深度探索C++对象模型》是由Stanley B. Lippman于1994年所著的一本经典畅销的C++书籍,该书详细介绍了C++对象模型的内部实现细节。 C++对象模型是指C++编译器在处理对象、继承、多态等面向对象特性时所采用的具体实现方式。这本书通过对对象模型的剖析,帮助读者深入理解C++的内部工作原理,从而写出更高效、更可靠的C++代码。 在《深度探索C++对象模型》中,作者首先介绍了对象、虚函数、继承等C++核心概念,然后详细讲解了C++对象模型的构建过程,包括对象布局、成员函数指针、虚函数表等。作者逐步深入地剖析了C++对象模型在内存中的表示方式,解释了为什么C++可以支持如此强大的面向对象特性。 此外,本书还探讨了一些高级主题,如多重继承、虚拟继承、构造函数和析构函数的执行顺序等。对于想要深入学习C++的读者来说,这本书提供了一些宝贵的技术手册和实用的经验。 尽管《深度探索C++对象模型》的出版时间是1994年,但它仍然被广泛认可为学习C++对象模型的经典之作。在2012年时,由于C++的发展和演进,也许一些内容已经有些过时,但很多基本概念和原理仍然适用。 总而言之,《深度探索C++对象模型》是一本值得阅读的C++经典著作,通过深度探索C++对象模型,读者可以更加深入地了解C++的内部工作原理和实现方式,提升自己的开发技能。 ### 回答3: 《深度探索C++对象模型》是一本于2012年出版的书籍。该书的作者Andrews和Sorkin以全面的角度深入探讨了C++对象模型。该书重点介绍了C++中的对象表示、虚函数、继承、多重继承、构造函数、析构函数等内容,以及与之相关的语法、原理和底层实现。 这本书为读者揭示了C++对象模型的奥秘,让人更加深入地理解C++语言中的类和对象。作者通过分析对象布局、虚函数表、虚函数调用、多继承中的数据布局和函数调用等等,解释了C++对象模型的实现机制。 在读者了解C++对象模型的基础上,该书还介绍了如何有效地利用对象模型来提高程序的性能。作者讨论了虚函数的成本以及如何减少虚函数调用的开销,提供了一些优化技巧。此外,书中还对C++的构造函数和析构函数进行了深入的讨论,详细解释了构造函数和析构函数的执行机制和注意事项。 总的来说,《深度探索C++对象模型》是一本深入剖析C++对象模型的重要参考书籍。通过阅读该书,读者可以更加全面地了解C++的类和对象的实现原理,对于理解C++语言的底层机制和优化程序性能具有积极的作用。无论是对于初学者还是有一定C++基础的开发人员来说,该书都是一本值得阅读的重要参考书。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值