编写更快的托管代码:了解开销情况

Jan Gray
Microsoft CLR Performance Team

2003 年 6 月

适用于:
   Microsoft® .NET Framework

摘要:本文介绍托管代码执行时间的低级操作开销模型,该模型是通过测量操作时间得到的,开发人员可以据此做出更好的编码决策并编写更快的代码。

下载 CLR Profiler。(330KB)

目录

简介(和誓言)
关于托管代码的开销模型
托管代码的开销情况
小结
资源

简介(和誓言)

实现计算的方法有无数种,但这些方法良莠不齐,有些方法远胜于其他方法:更简单,更清晰,更容易维护。有些方法速度很快,有些却慢得出奇。

不要错用那些速度慢、内容臃肿的代码。难道您不讨厌这样的代码吗:不能连续运行的代码、不时将用户界面锁定几秒种的代码、顽固占用 CPU 或严重损害磁盘的代码?

千万不要用这样的代码。相反,请站起来,和我一起宣誓:

“我保证,我不会向用户提供慢速代码。速度是我关注的特性。每天我都会注意代码的性能。我会经常地、系统地‘测量’代码的速度和大小。我将学习、构建或购买为此所需的工具。这是我的责任。”

(我保证。)你是这样保证的吗?非常好。

那么,怎样才能在日常工作中编写出最快、最简洁的代码呢?这就要不断有意识地优先选择节俭的方法,而不要选择浪费、臃肿的方法,并且要深入思考。即使是任意指定的一段代码,都会需要许多这样的小决定。

但是,如果不知道开销的情况,就无法面对众多方案作出明智的选择:如果您不知道开销情况,也就无法编写高效的代码。

在过去的美好日子里,事情要容易一些,好的 C 程序员都知道。C 中的每个运算符和操作,不管是赋值、整数或浮点数学、解除引用,还是函数调用,都在不同程度上一一对应着单一的原始计算机操作。当然,有时会需要数条计算机指令来将正确的操作数放置在正确的寄存器中,而有时一条指令就可以完成几种 C 操作(比较著名的是 *dest++ = *src++;),但您通常可以编写(或阅读取)一行 C 代码,并知道要花费多少时间。对于代码和数据,C 编译器具有所见即所得的特点 -“您编写的就是您得到的”。(例外的情况是函数调用。如果不知道函数的开销,您将无法知道其花费的时间。)

到了 20 世纪 90 年代,为了将数据抽象、面向对象编程和代码复用等技术更好地用于软件工程和生产,PC 软件业将 C 发展为 C++。

C++ 是 C 的超集,并且是“使用才需付出”,即如果不使用,新功能不会有任何开销。因此,C 的专用编程技术,包括其内在的开销模型,都可以直接应用。如果编写一段 C 代码并用 C++ 重新编译这段代码,则执行时间和空间的系统开销不会有太大变化。

另一方面,C++ 引入了许多新的语言功能,包括构造函数、析构函数、New、Delete、单继承、多继承、虚拟继承、数据类型转换、成员函数、虚函数、重载运算符、指向成员的指针、对象数组、异常处理和相同的复合,这些都会造成许多不易察觉但非常重要的开销。例如,每次调用虚函数时都要花费两次额外的定位,而且还会将隐藏的 vtable 指针字段添加到每个实例中。或者,考虑将这段看起来比较安全的代码:

{ complex a, b, c, d; ... a = b + c * d; }

编译为大约十三个隐式成员函数调用(但愿是内联的)。

九年前,在我的文章 C++:Under the Hood(英文)中曾探讨过这个主题,我写道:

“了解编程语言的实现方式是非常重要的。这些知识可以让我们消除‘编译器到底在做些什么?’的恐惧和疑虑,让我们有信心使用新功能,并使我们在调试和学习其他的语言功能时更具洞察力。这些知识还能使我们认识到各种编码方案的相对开销,而这正是我们在日常工作中编写出最有效的代码所必需的。”

现在,我们将以同样的方式来了解托管代码。本文将探讨托管执行的“低级”时间和空间开销,以使我们能够在日常的编码工作中权衡利弊,做出明智的判断。

并遵守我们的承诺。

为什么是托管代码?

对大多数本机代码的开发人员来说,托管代码为运行他们的软件提供了更好、更有效率的平台。它可以消除整类错误,如堆损坏和数组索引超出边界的错误,而这些错误常常使深夜的调试工作无功而返。它支持更为现代的要求,如安全移动代码(通过代码访问安全性实现)和 XML Web Service,而且与过去的 Win32/COM/ATL/MFC/VB 相比,.NET Framework 更加清楚明了,利用它可以做到事半功倍。

对软件用户来说,托管代码为他们提供了更丰富、更健壮的应用程序,让他们通过更优质的软件享受更好的生活。

编写更快的托管代码的秘诀是什么?

尽管可以做到事半功倍,但还是不能放弃认真编码的责任。首先,您必须承认:“我是个新手。”您是个新手。我也是个新手。在托管代码领域中,我们都是新手。我们仍然在学习这方面的诀窍,包括开销的情况。

面对功能丰富、使用方便的 .NET Framework,我们就像糖果店里的孩子:“哇,不需要枯燥的 strncpy,只要把字符串‘+’在一起就可以了!哇,我可以在几行代码中加载一兆字节的 XML!哈哈!”

一切都是那么容易。真的是很容易。即使是从 XML 信息集中提出几个元素,也会轻易地投入几兆字节的 RAM 来分析 XML 信息集。使用 C 或 C++ 时,这件事是很令人头疼的,必须考虑再三,甚至您会想在某些类似 SAX 的 API 上创建一个状态机。而使用 .NET Framework 时,您可以在一口气加载整个信息集,甚至可以反复加载。这样一来,您的应用程序可能就不再那么快了。也许它的工作集达到了许多兆字节。也许您应该重新考虑一下那些简单方法的开销情况。

遗憾的是,在我看来,当前的 .NET Framework 文档并没有足够详细地介绍 Framework 的类型和方法的性能含义,甚至没有具体指明哪些方法会创建新对象。性能建模不是一个很容易阐述的主题,但是“不知道”会使我们更难做出恰当的决定。

既然在这方面我们都是新手,又不知道任何开销情况,而且也没有什么文档可以清楚说明开销情况,那我们应该做些什么呢?

测量,对开销进行测量。秘诀就是“对开销进行测量”并“保持警惕”。我们都应该养成测量开销的习惯。如果我们不怕麻烦去测量开销,就不会轻易调用比我们“假设”的开销高出十倍的新方法。

(顺便说一下,要更深入地了解 BCL [基类库] 的性能基础或 CLR,请查看 Shared Source CLI [英文],又称 Rotor。Rotor 代码与 .NET Framework 和 CLR 属于同一类别,但并不是完全相同的代码。不过即使是这样,我保证在认真学习 Rotor 之后,您会对 CLR 有更新、更深刻的理解。但一定保证首先要审核 SSCLI 许可证!)

知识

如果您想成为伦敦的出租车司机,首先必须学习 The Knowledge(英文)。学生们通过几个月的学习,要记住伦敦城里上千条的小街道,还要了解到达各个地点的最佳路线。他们每天骑着踏板车四处查看,以巩固在书本上学到的知识。

同样,如果您想成为一名高性能托管代码的开发人员,您必须获得“托管代码知识”。您必须了解每项低级操作的开销,必须了解像委托 (Delegate) 和代码访问安全等这类功能的开销,还必须了解正在使用以及正在编写的类型和方法的开销。能够发现哪些方法的开销太大,对您的应用程序不会有什么损害,反倒因此可以避免使用这些方法。

这些知识不在任何书本中,也就是说,您必须骑上自己的踏板车进行探索:准备好 csc、ildasm、VS.NET 调试器、CLR 分析器、您的分析器、一些性能计时器等,了解代码的时间和空间开销。

关于托管代码的开销模型

让我们开门见山地谈谈托管代码的开销模型。利用这种模型,您可以查看叶方法,能马上判断出开销较大的表达式或语句,而在您编写新代码时,就可以做出更明智的选择。

(有关调用您的方法或 .NET Framework 方法所需的可传递的开销,本文将不做介绍。这些内容以后会在另一篇文章中介绍。)

之前我曾经说过,大多数的 C 开销模型仍然适用于 C++ 方案。同样,许多 C/C++ 开销模型也适用于托管代码。

怎么会这样呢?您一定了解 CLR 执行模型。您使用几种语言中的一种来编写代码,并将其编译成 CIL(公用中间语言)格式,然后打包成程序集。当您运行主应用程序的程序集时,它开始执行 CIL。但是不是像旧的字节码解释器一样,速度会非常慢?

实时编译器

不,它一点也不慢。CLR 使用 JIT(实时)编译器将 CIL 中的各种方法编译成本机 x86 代码,然后运行本机代码。尽管 JIT 在编译首次调用的方法时会稍有延迟,但所调用的各种方法在运行纯本机代码时都不需要解释性的系统开销。

与传统的脱机 C++ 编译过程不同,JIT 编译器花费的时间对用户来说都是“时钟时间”延迟,因此 JIT 编译器不具备占用大量时间的彻底优化过程。尽管如此,JIT 编译器所执行的一系列优化仍给人以深刻印象:

  • 常量重叠
  • 常量和复制的传播
  • 通用子表达式消除
  • 循环不变量的代码活动
  • 死存储 (Dead Store) 和死代码 (Dead Code) 消除
  • 寄存器分配
  • 内联方法
  • 循环展开(带有小循环体的小循环)

结果可以与传统的本机代码相媲美,至少是相近。

至于数据,可以混合使用值类型和引用类型。值类型(包括整型、浮点类型、枚举和结构)通常存储在栈中。这些数据类型就像 C/C++ 中的本地和结构一样又小又快。使用 C/C++ 时,应该避免将大的结构作为方法参数或返回值进行传送,因为复制的系统开销可能会大的惊人。

引用类型和装箱后的值类型存储在堆中。它们通过对象引用来寻址,这些对象引用只是计算机的指针,就像 C/C++ 中的对象指针一样。

因此实时编译的托管代码可以很快。下面我们将讨论一些例外,如果您深入了解了本机 C 代码中某些表达式的开销,您就不会像在托管代码中那样错误地为这些开销建模。

我还应该提一下 NGEN,这是一种“超前的”工具,可以将 CIL 编译为本机代码程序集。尽管利用 NGEN 编译程序集在当前并不会对执行时间造成什么实质性的影响(好的或坏的影响),却会使加载到许多应用程序域和进程中的共享程序集的总工作集减少。(操作系统可以跨所有客户端共享一份利用 NGEN 编译的代码,而实时编译的代码目前通常不会跨应用程序域或进程共享。请参阅 LoaderOptimizationAttribute.MultiDomain [英文]。)

自动内存管理

托管代码与本机代码的最大不同之处在于自动内存管理。您可以分配新的对象,但 CLR 垃圾回收器 (GC) 会在这些对象无法访问时自动释放它们。GC 不时地运行,通常不为人觉察,但一般会使应用程序停止一两毫秒,偶尔也会更长一些。

有一些文章探讨了垃圾回收器的性能含义,这里就不作介绍了。如果您的应用程序遵循这些文章中的建议,那么总的内存回收开销就不会很大,也就是百分之几的执行时间,与传统的 C++ 对象 newdelete 大致相当或者更好一些。创建对象以及后来的自动收回对象的分期开销非常低,这样就可以在每秒钟内创建数千万个小对象。

但仍不能“免费”分配对象。对象会占用空间。无限制的对象分配将会导致更加频繁的内存回收。

更糟糕的是,不必要地持续引用无用的对象图 (Object Graph) 会使对象保持活动。有时,我们会发现有些不大的程序竟然有 100 MB 以上的工作集,可是这些程序的作者却拒绝承认自己的错误,反而认为性能不佳是由于托管代码本身存在一些神秘、无法确认(因此很难处理)的问题。这真令人遗憾。但是,只需使用 CLR 编译器花一个小时做一下研究,更改几行代码,就可以将这些程序用到的堆减少十倍或更多。如果您遇上大的工作集问题,第一步就应该查看真实的情况。

因此,不要创建不必要的对象。由于自动内存管理消除了许多对象分配和释放方面的复杂情况、问题和错误,并且用起来又快又方便,因此我们会很自然地想要创建越来越多的对象,最终形成错综复杂的对象群。如果您想编写真正的快速托管代码,创建对象时就需要深思熟虑,确保对象的数量合适。

这也适用于 API 的设计。由于可以设计类型及其方法,因此它们会要求客户端创建可以随便放弃的新对象。不要那样做。

托管代码的开销情况

现在,让我们来研究一下各种低级托管代码操作的时间开销。

表 1 列出了各种低级托管代码操作的大致开销,单位是毫微秒。这些数据是在配备了 1.1 GHz Pentium-III、运行了 Windows XP 和 .NET Framework v1.1 (Everett) 的静止 PC 上通过一套简单的计时循环收集到的。

测试驱动程序调用各种测试方法,指定要执行的多个迭代,自动调整为迭代 218 到 230 次,并根据需要使每次测试的时间不少于 50 毫秒。一般情况下,这么长的时间足可以在一个进行密集对象分配的测试中观察几个 0 代内存回收周期。该表显示了 10 次实验的平均结果,对于每个测试主题,都列出了最好(最少时间)的实验结果。

根据需要,每个测试循环都展开 4 至 60 次,以减少测试循环的系统开销。我检查了每次测试生成的主机代码,以确保 JIT 编译器没有将测试彻底优化,例如,我修改了几个示例中的测试,以使中间结果在测试循环期间和测试循环之后都存在。同样,我还对几个测试进行了更改,以使通用子表达式消除不起作用。

表 1:原语时间(平均和最小)(ns)

平均 最小 原语 平均 最小 原语 平均 最小 原语
0.0 0.0 Control 2.6 2.6 new valtype L1 0.8 0.8 isinst up 1
1.0 1.0 Int add 4.6 4.6 new valtype L2 0.8 0.8 isinst down 0
1.0 1.0 Int sub 6.4 6.4 new valtype L3 6.3 6.3 isinst down 1
2.7 2.7 Int mul 8.0 8.0 new valtype L4 10.7 10.6 isinst (up 2) down 1
35.9 35.7 Int div 23.0 22.9 new valtype L5 6.4 6.4 isinst down 2
2.1 2.1 Int shift 22.0 20.3 new reftype L1 6.1 6.1 isinst down 3
2.1 2.1 long add 26.1 23.9 new reftype L2 1.0 1.0 get field
2.1 2.1 long sub 30.2 27.5 new reftype L3 1.2 1.2 get prop
34.2 34.1 long mul 34.1 30.8 new reftype L4 1.2 1.2 set field
50.1 50.0 long div 39.1 34.4 new reftype L5 1.2 1.2 set prop
5.1 5.1 long shift 22.3 20.3 new reftype empty ctor L1 0.9 0.9 get this field
1.3 1.3 float add 26.5 23.9 new reftype empty ctor L2 0.9 0.9 get this prop
1.4 1.4 float sub 38.1 34.7 new reftype empty ctor L3 1.2 1.2 set this field
2.0 2.0 float mul 34.7 30.7 new reftype empty ctor L4 1.2 1.2 set this prop
27.7 27.6 float div 38.5 34.3 new reftype empty ctor L5 6.4 6.3 get virtual prop
1.5 1.5 double add 22.9 20.7 new reftype ctor L1 6.4 6.3 set virtual prop
1.5 1.5 double sub 27.8 25.4 new reftype ctor L2 6.4 6.4 write barrier
2.1 2.0 double mul 32.7 29.9 new reftype ctor L3 1.9 1.9 load int array elem
27.7 27.6 double div 37.7 34.1 new reftype ctor L4 1.9 1.9 store int array elem
0.2 0.2 inlined static call 43.2 39.1 new reftype ctor L5 2.5 2.5 load obj array elem
6.1 6.1 static call 28.6 26.7 new reftype ctor no-inl L1 16.0 16.0 store obj array elem
1.1 1.0 inlined instance call 38.9 36.5 new reftype ctor no-inl L2 29.0 21.6 box int
6.8 6.8 instance call 50.6 47.7 new reftype ctor no-inl L3 3.0 3.0 unbox int
0.2 0.2 inlined this inst call 61.8 58.2 new reftype ctor no-inl L4 41.1 40.9 delegate invoke
6.2 6.2 this instance call 72.6 68.5 new reftype ctor no-inl L5 2.7 2.7 sum array 1000
5.4 5.4 virtual call 0.4 0.4 cast up 1 2.8 2.8 sum array 10000
5.4 5.4 this virtual call 0.3 0.3 cast down 0 2.9 2.8 sum array 100000
6.6 6.5 interface call 8.9 8.8 cast down 1 5.6 5.6 sum array 1000000
1.1 1.0 inst itf instance call 9.8 9.7 cast (up 2) down 1 3.5 3.5 sum list 1000
0.2 0.2 this itf instance call 8.9 8.8 cast down 2 6.1 6.1 sum list 10000
5.4 5.4 inst itf virtual call 8.7 8.6 cast down 3 22.0 22.0 sum list 100000
5.4 5.4 this itf virtual call       21.5 21.4 sum list 1000000

免责声明:请不要照搬这些数据。时间测试会由于无法预料的二次影响而变得不准确。偶然事件可能会使实时编译的代码或某些关键数据跨过缓存行,影响其他的缓存或已有数据。这有点像不确定性原则:1 毫微秒左右的时间和时间差异是可观察到的范围限度。

另一项免责声明:这些数据只与完全适应缓存的小代码和数据方案有关。如果应用程序中最常用的部分不适应芯片缓存,您可能会遇到其他的性能问题。本文的结尾将详细介绍缓存。

还有一项免责声明:将组件和应用程序作为 CIL 的程序集的最大好处之一是,您的程序可以做到每秒都变快、每年都变快。“每秒都变快”是因为运行时(理论上)可以在程序运行时重新调整 JIT 编译的代码;“每年都变快”是因为新发布的运行时总能提供更好、更先进、更快的算法以将代码迅速优化。因此,如果 .NET 1.1 中的这几个计时不是最佳结果,请相信在以后发布的产品中它们会得到改善。而且在今后发布的 .NET Framework 中,本文中所列代码的本机代码序列可能会更改。

不考虑这些免责声明,这些数据确实让我们对各种原语的当前性能有了充分的认识。这些数字很有意义,并且证实了我的判断,即大多数实时编译的托管代码可以像编译过的本机代码一样,“接近计算机”运行。原始的整型和浮点操作很快,而各种方法调用却不太快,但(请相信我)仍可比得上本机 C/C++。同时我们还会发现,有些通常在本机代码中开销不太大的操作(如数据类型转换、数组和字段存储、函数指针 [委托])现在的开销却变大了。为什么是这样呢?让我们来看一下。

。。。。

资源

  • David Stutz et al,《Shared Source CLI Essentials》。O'Reilly and Assoc.,2003。ISBN 059600351X。
  • Jan Gray,C++:Under the Hood(英文)。
  • Gregor Noriskin,编写高性能的托管应用程序:入门,MSDN。
  • Rico Mariani,Garbage Collector Basics and Performance Hints(英文),MSDN。
  • Emmanuel Schanzer,Performance Tips and Tricks in .NET Applications(英文),MSDN。
  • Emmanuel Schanzer,Performance Considerations for Run-Time Technologies in the .NET Framework(英文),MSDN。
  • vadump(平台 SDK 工具)(英文),MSDN。
  • .NET 演示,[Managed] Code Optimization(英文),2002 年 9 月 10 日,MSDN。 

 

Visual C and C++ (General) Technical Articles  

C++: Under the Hood

Jan Gray

March 1994

Jan Gray is a Software Design Engineer in Microsoft’s Visual C++ Business Unit. He helped design and implement the Microsoft Visual C++ compiler.

Introduction

It is important to understand how your programming language is implemented. Such knowledge dispels the fear and wonder of “What on earth is the compiler doing here?”; imparts confidence to use the new features; and provides insight when debugging and learning other language features. It also gives a feel for the relative costs of different coding choices that is necessary to write the most efficient code day to day.

This paper looks “under the hood” of C++, explaining “run-time” C++ implementation details such as class layout techniques and the virtual function call mechanism. Questions to be answered include:

  • How are classes laid out?
  • How are data members accessed?
  • How are member functions called?
  • What is an adjuster thunk?
  • What are the costs:
    • Of single, multiple, and virtual inheritance?
    • Of virtual functions and virtual function calls?
    • Of casts to bases, to virtual bases?
    • Of exception handling?

First, we’ll look at struct layout of C-like structs, single inheritance, multiple inheritance, and virtual inheritance, then consider data member access and member functions, virtual and not. We’ll examine the workings of constructors, destructors, and assignment operator special member functions and dynamic construction and destruction of arrays. Finally, we’ll briefly consider the impact of exception-handling support.

For each language feature topic, we’ll very briefly present motivation and semantics for the language feature (although “Introduction to C++” this is not), and examine how the language feature was implemented in Microsoft® Visual C++™. Note the distinction between abstract language semantics and a particular concrete implementation. Other vendors have sometimes made different implementation choices for whatever reasons. In a few cases we contrast the Visual C++ implementation with others.

Class Layout

In this section we’ll consider the storage layouts required for different kinds of inheritance.

C-like Structs

As C++ is based upon C, it is “mostly” upwards-compatible with C. In particular, the working papers specify the same simple struct layout rules that C has: Members are laid out in their declaration order, subject to implementation defined alignment padding. All C/C++ vendors ensure that valid C structs are stored identically by their C and C++ compilers. Here A is a simple C struct with the obvious expected member layout and padding.

  
struct A {
   char c;
   int i;
};
 
  

C-like Structs with C++ Features

Of course, C++ is an object-oriented programming language: It provides inheritance, encapsulation, and polymorphism by extending the mundane C struct into the wondrous C++ class. Besides data members, C++ classes can also encapsulate member functions and many other things. However, except for hidden data members introduced to implement virtual functions and virtual inheritance, the instance size is solely determined by a class’s data members and base classes.

Here B is a C-like struct with some C++ features: There are public/protected/private access control declarations, member functions, static members, and nested type declarations. Only the non-virtual data members occupy space in each instance. Note that the standards committee working papers permit implementations to reorder data members separated by an access declarator, so these three members could have been laid out in any order. (In Visual C++, members are always laid out in declaration order, just as if they were members of a C struct)

  
struct B {
public:
   int bm1;
protected:
   int bm2;
private:
   int bm3;
   static int bsm;
   void bf();
   static void bsf();
   typedef void* bpv;
   struct N { };
};
 
  

Single Inheritance

C++ provides inheritance to factor out and share common aspects of different types. A good example of a classes-with-inheritance data type organization is biology’s classification of living things into kingdoms, phyla, orders, families, genus, species, and so on. This organization makes it possible to specify attributes, such as “mammals bear live young” at the most appropriate level of classification; these attributes are then inherited by other classes, so we can conclude without further specification that whales, squirrels, and people bear live young. Exceptional cases, such as platypi (a mammal, yet lays eggs), will require that we override the inherited attribute or behavior with one more appropriate for the derived class. More on that later.

In C++, inheritance is specified by using the “: base” syntax when defining the derived class. Here D is derived from its base class C.

struct C {
   int c1;
   void cf();
};
 
  

struct D : C {
   int d1;
   void df();
};
 
  

Since a derived class inherits all the properties and behavior of its base class, each instance of the derived class will contain a complete copy of the instance data of the base class. Within D, there is no requirement that C’s instance data precede D’s. But by laying D out this way, we ensure that the address of the C object within D corresponds to the address of the first byte of the D object. As we shall see, this eliminates adding a displacement to a D* when we need to obtain the address of its embedded C. This layout is used by all known C++ implementations. Thus, in a single inheritance class hierarchy, new instance data introduced in each derived class is simply appended to the layout of the base class. Note our layout diagram labels the “address points” of pointers to the C and D objects within a D.

Multiple Inheritance

Single inheritance is quite versatile and powerful, and generally adequate for expressing the (typically limited) degree of inheritance present in most design problems. Sometimes, however, we have two or more sets of behavior that we wish our derived class to acquire. C++ provides multiple inheritance to combine them.

For instance, say we have a model for an organization that has a class Manager (who delegates) and class Worker (who actually does the work). Now how can we model a class MiddleManager, who, like a Worker, accepts work assignments from his/her manager and who, like a Manager, delegates this work to his/her employees? This is awkward to express using single inheritance: For MiddleManager to inherit behavior from both Manager and Worker, both must be base classes. If this is arranged so that MiddleManager inherits from Manager which inherits from Worker, it erroneously ascribes Worker behavior to Managers. (Vice versa, the same problem.) Of course, MiddleManager could inherit from just one (or neither) of Worker or Manager, and instead, duplicate (redeclare) both interfaces, but that defeats polymorphism, fails to reuse the existing interface, and leads to maintenance woes as interfaces evolve over time.

Instead, C++ allows a class to inherit from multiple base classes:

struct Manager ... { ... };
struct Worker ... { ... };
struct MiddleManager : Manager, Worker { ... };
 
  

How might this be represented? Continuing with our “classes of the alphabet” example:

  
struct E {
   int e1;
   void ef();
};
  

  
struct F : C, E {
   int f1;
   void ff();
  
  
};
  

Struct F multiply inherits from C and E. As with single inheritance, F contains a copy of the instance data of each of its base classes. Unlike single inheritance, it is not possible to make the address point of each bases’ embedded instance correspond to the address of the derived class:

  
F f;
// (void*)&f == (void*)(C*)&f;
// (void*)&f <  (void*)(E*)&f;
  

Here, the address point of the embedded E within F is not at the address of the F itself. As we shall see when we consider casts and member functions, this displacement leads to a small overhead that single inheritance does not generally require.

An implementation is free to lay out the various embedded base instances and the new instance data in any order. Visual C++ is typical in laying out the base instances in declaration order, followed by the new data members, also in declaration order. (As we shall see, this is not necessarily the case when some bases have virtual functions and others don’t).

Virtual Inheritance

Returning to the MiddleManager example which motivated multiple inheritance in the first place, we have a problem. What if both Manager and Worker are derived from Employee?

  
struct Employee { ... };
struct Manager : Employee { ... };
struct Worker : Employee { ... };
struct MiddleManager : Manager, Worker { ... };
  

Since both Worker and Manager inherit from Employee, they each contain a copy of the Employee instance data. Unless something is done, each MiddleManager will contain two instances of Employee, one from each base. If Employee is a large object, this duplication may represent an unacceptable storage overhead. More seriously, the two copies of the Employee instance might get modified separately or inconsistently. We need a way to declare that Manager and Worker are each willing to share a single embedded instance of their Employee base class, should Manager or Worker ever be inherited with some other class that also wishes to share the Employee base class.

In C++, this “sharing inheritance” is (unfortunately) called virtual inheritance and is indicated by specifying that a base class is virtual.

  
struct Employee { ... };
struct Manager : virtual Employee { ... };
struct Worker : virtual Employee { ... };
struct MiddleManager : Manager, Worker { ... };
  

Virtual inheritance is considerably more expensive to implement and use than single and multiple inheritance. Recall that for single (and multiple) inherited bases and derived classes, the embedded base instances and their derived classes either share a common address point (as with single inheritance and the leftmost base inherited via multiple inheritance), or have a simple constant displacement to the embedded base instance (as with multiple inherited non-leftmost bases, such as E). With virtual inheritance, on the other hand, there can (in general) be no fixed displacement from the address point of the derived class to its virtual base. If such a derived class is further derived from, the further deriving class may have to place the one shared copy of the virtual base at some other, different offset in the further derived class. Consider this example:

  
struct G : virtual C {
   int g1;
   void gf();
};
  

  
struct H : virtual C {
   int h1;
   void hf();
};
  

  
struct I : G, H {
   int i1;
   void _if();
};
  

Ignoring the vbptr members for a moment, notice that within a G object, the embedded C immediately follows the G data member, and similarly notice that within an H, the embedded C immediately follows the H data member. Now when we layout I, we can’t preserve both relationships. In the Visual C++ layout above, the displacements from G to C in a G instance and in an I instance are different. Since classes are generally compiled without knowledge of how they will be derived from, each class with a virtual base must have a way to compute the location of the virtual base from the address point of its derived class.

In Visual C++, this is implemented by adding a hidden vbptr (“virtual base table pointer”) field to each instance of a class with virtual bases. This field points to a shared, per-class table of displacements from the address point of the vbptr field to the class’s virtual base(s).

Other implementations use embedded pointers from the derived class to its virtual bases, one per base. This other representation has the advantage of a smaller code sequence to address the virtual base, although an optimizing code generator can often common-subexpression-eliminate repeated virtual base access computations. However, it also has the disadvantages of larger instance sizes for classes with multiple virtual bases, of slower access to virtual bases of virtual bases (unless one incurs yet further hidden pointers), and of a less regular pointer to member dereference.

In Visual C++, G has a hidden vbptr which addresses a virtual base table whose second entry is GdGvbptrC. (This is our notation for “in G, the displacement from G’s vbptr to C”. (We omit the prefix to “d” if the quantity is constant in all derived classes.)) For example, on a 32-bit platform, GdGvbptrC would be 8 (bytes). Similarly, the embedded G instance within an I addresses a vbtable customized for G’s within I’s, and so IdGvbptrC would be 20.

As can be seen from the layouts of G, H, and I, Visual C++ lays out classes with virtual bases by:

  • Placing embedded instances of the non-virtually inherited bases first,
  • Adding a hidden vbptr unless a suitable one was inherited from one of the non-virtual bases,
  • Placing the new data members declared in the derived class, and, finally,
  • Placing a single instance of each of the virtually inherited bases at the end of the instance.

This representation lets the virtually inherited bases “float” within the derived class (and its further derived classes) while keeping together and at constant relative displacements those parts of the object that are not virtual bases.

Data Member Access

Now that we have seen how classes are laid out, let’s consider the cost to access data members of these classes.

No inheritance. In absence of inheritance, data member access is the same as in C: a dereference off some displacement from the pointer to the object.

  
C* pc;
  
  
pc->c1; // *(pc + dCc1);
  

Single inheritance. Since the displacement from the derived object to its embedded base instance is a constant 0, that constant 0 can be folded with the constant offset of the member within that base.

  
D* pd;
pd->c1; // *(pd + dDC + dCc1); // *(pd + dDCc1);
pd->d1; // *(pd + dDd1);
  

Multiple inheritance. Although the displacement to a given base, or to a base of a base, and so on, might be non-zero, it is still constant, and so any set of such displacements can be folded together into one constant displacement off the object pointer. Thus even with multiple inheritance, access to any member is inexpensive.

  
F* pf;
pf->c1; // *(pf + dFC + dCc1); // *(pf + dFc1);
pf->e1; // *(pf + dFE + dEe1); // *(pf + dFe1);
pf->f1; // *(pf + dFf1);
  

Virtual inheritance. Within a class with virtual bases, access to a data member or non-virtually inherited base class is again just a constant displacement off the object pointer. However, access to a data member of a virtual base is comparatively expensive, since it is necessary to fetch the vbptr, fetch a vbtable entry, and then add that displacement to the vbptr address point, just to compute the address of the data member. However, as shown for i.c1 below, if the type of the derived class is statically known, the layout is also known, and it is unnecessary to load a vbtable entry to find the displacement to the virtual base.

  
I* pi;
pi->c1; // *(pi + dIGvbptr + (*(pi+dIGvbptr))[1] + dCc1);
pi->g1; // *(pi + dIG + dGg1); // *(pi + dIg1);
pi->h1; // *(pi + dIH + dHh1); // *(pi + dIh1);
pi->i1; // *(pi + dIi1);
I i;
i.c1; // *(&i + IdIC + dCc1); // *(&i + IdIc1);
  

What about access to members of transitive virtual bases, for example, members of virtual bases of virtual bases (and so on)? Some implementations follow one embedded virtual base pointer to the intermediate virtual base, then follow its virtual base pointer to its virtual base, and so on. Visual C++ optimizes such access by using additional vbtable entries which provide displacements from the derived class to any transitive virtual bases.

Casts

Except for classes with virtual bases, it is relatively inexpensive to explicitly cast a pointer into another pointer type. If there is a base-derived relationship between class pointers, the compiler simply adds or subtracts the displacement between the two (often 0).

  
F* pf;
(C*)pf; // (C*)(pf ? pf + dFC : 0); // (C*)pf;
(E*)pf; // (E*)(pf ? pf + dFE : 0);
  

In the C* cast, no computations are required, because dFC is 0. In the E* cast, we must add dFE, a non-zero constant, to the pointer. C++ requires that null pointers (0) remain null after a cast. Therefore Visual C++ checks for null before performing the addition. This check occurs only when a pointer is implicitly or explicitly converted to a related pointer type, not when a derived* is implicitly converted to a base*const this pointer when a base member function is invoked on a derived object.

As you might expect, casting over a virtual inheritance path is relatively expensive: about the same cost as accessing a member of a virtual base:

  
I* pi;
(G*)pi; // (G*)pi;
(H*)pi; // (H*)(pi ? pi + dIH : 0);
(C*)pi; // (C*)(pi ? (pi+dIGvbptr + (*(pi+dIGvbptr))[1]) : 0);
  

In general, you can avoid a lot of expensive virtual base field accesses by replacing them with one cast to the virtual base and base relative accesses:

  
/* before: */             ... pi->c1 ... pi->c1 ...
/* faster: */ C* pc = pi; ... pc->c1 ... pc->c1 ...
  

Member Functions

A C++ member function is just another member in the scope of its class. Each (non-static) member function of a class X receives a special hidden this parameter of type X *const, which is implicitly initialized from the object the member function is applied to. Also, within the body of a member function, member access off the this pointer is implicit.

  
struct P {
   int p1;
   void pf(); // new
   virtual void pvf(); // new
  
  
};
  

P has a non-virtual member function pf() and a virtual member function pvf(). It is apparent that virtual member functions incur an instance size hit, as they require a virtual function table pointer. More on that later. Notice there is no instance cost to declaring non-virtual member functions. Now consider the definition of P::pf():

 

  
void P::pf() { // void P::pf([P *const this])
   ++p1;   // ++(this->p1);
}
  

Here P::pf() receives a hidden this parameter, which the compiler has to pass each call. Also note that member access can be more expensive than it looks, because member accesses are this relative. On the other hand, compilers commonly enregister this so member access cost is often no worse than accessing a local variable. On the other hand, compilers may not be able to enregister the instance data itself because of the possibility this is aliased with some other data.

Overriding Member Functions

Member functions are inherited just as data members are. Unlike data members, a derived class can override, or replace, the actual function definition to be used when an inherited member function is applied to a derived instance. Whether the override is static (determined at compile time by the static types involved in the member function call) or dynamic (determined at run-time by the dynamic object addressed by the object pointer) depends upon whether the member function is declared virtual.

Class Q inherits P’s data and function members. It declares pf(), overriding P::pf(). It also declares pvf(), a virtual function overriding P::pvf(), and declares a new non-virtual member function qf(), and a new virtual function qvf().

  
struct Q : P {
   int q1;
   void pf();  // overrides P::pf
   void qf();  // new
   void pvf(); // overrides P::pvf
   virtual void qvf(); // new
  
  
};
  

For non-virtual function calls, the member function to call is statically determined, at compile time, by the type of the pointer expression to the left of the -> operator. In particular, even though ppq points to an instance of Q, ppq->pf() calls P::pf(). (Also notice the pointer expression left of the -> is passed as the hidden this parameter.)

  
P p; P* pp = &p; Q q; P* ppq = &q; Q* pq = &q;
pp->pf();  // pp->P::pf();  // P::pf(pp);
ppq->pf(); // ppq->P::pf(); // P::pf(ppq);
pq->pf();  // pq->Q::pf();  // Q::pf((P*)pq);
pq->qf();  // pq->Q::qf();  // Q::qf(pq);
  

For virtual function calls, the member function to call is determined at run-time. Regardless of the declared type of the pointer expression left of the -> operator, the virtual function to call is the one appropriate to the type of the actual instance addressed by the pointer. In particular, although ppq has type P*, it addresses a Q, and so Q::pvf() is called.

  
pp->pvf();  // pp->P::pvf();  // P::pvf(pp);
ppq->pvf(); // ppq->Q::pvf(); // Q::pvf((Q*)ppq);
pq->pvf();  // pq->Q::pvf();  // Q::pvf((P*)pq);
  

Hidden vfptr members are introduced to implement this mechanism. A vfptr is added to a class (if it doesn’t already have one) to address that class’s virtual function table (vftable). Each virtual function in a class has a corresponding entry in that class’s vftable. Each entry holds the address of the virtual function override appropriate to that class. Therefore, calling a virtual function requires fetching the instance’s vfptr, and indirectly calling through one of the vftable entries addressed by that pointer. This is in addition to the usual function call overhead of parameter passing, call, and return instructions. In the example below, we fetch q’s vfptr, which addresses Q’s vftable, whose first entry is &Q::pvf. Thus Q::pvf() is called.

Looking back at the layouts of P and Q, we see that the Visual C++ compiler has placed the hidden vfptr member at the start of the P and Q instances. This helps ensure that virtual function dispatch is as fast as possible. In fact, the Visual C++ implementation ensures that the first field in any class with virtual functions is always a vfptr. This can require inserting the new vfptr before base classes in the instance layout, or even require that a right base class that does begin with a vfptr be placed before a left base that does not have one.

Most C++ implementations will share or reuse an inherited base’s vfptr. Here Q did not receive an additional vfptr to address a table for its new virtual function qvf(). Instead, a qvf entry is appended to the end of P’s vftable layout. In this way, single inheritance remains inexpensive. Once an instance has a vfptr it doesn’t need another one. New derived classes can introduce yet more virtual functions, and their vftable entries are simply appended to the end of their one per-class vftable.

Virtual Functions: Multiple Inheritance

It is possible for an instance to contain more than one vfptr if it inherits them from multiple bases, each with virtual functions. Consider R and S:

  
struct R {
   int r1;
   virtual void pvf(); // new
 
   virtual void rvf(); // new
};
  

  
struct S : P, R {
   int s1;
   void pvf(); // overrides P::pvf and R::pvf
   void rvf(); // overrides R::rvf
   void svf(); // new
};
  

Here R is just another class with some virtual functions. Since S multiply inherits, from P and R, it contains an embedded instance of each, plus its own instance data contribution, S::s1. Notice the right base R has a different address point than do P and S, as expected with multiple inheritance. S::pvf() overrides both P::pvf() and R::pvf(), and S::rvf() overrides R::rvf(). Here are the required semantics for the pvf override:

  
S s; S* ps = &s;
((P*)ps)->pvf(); // ((P*)ps)->P::vfptr[0])((S*)(P*)ps)
((R*)ps)->pvf(); // ((R*)ps)->R::vfptr[0])((S*)(R*)ps)
ps->pvf();       // one of the above; calls S::pvf()
  

Since S::pvf() overrides both P::pvf() and R::pvf(), it must replace their vftable entries in the S vftables. However, notice that it is possible to invoke pvf() both as a P and an R. The problem is that R’s address point does not correspond to P’s and S’s. The expression (R*)ps does not point to the same part of the class as does (P*)ps. Since the function S::pvf() expects to receive an S* as its hidden this parameter, the virtual function call itself must automatically convert the R* at the call site into an S* at the callee. Therefore, S’s copy of R’s vftable’s pvf slot takes the address of an adjuster thunk, which applies the address adjustment necessary to convert an R* pointer into an S* as desired.

In MSC++, for multiple inheritance with virtual functions, adjuster thunks are required only when a derived class virtual function overrides virtual functions of multiple base classes.

Address Points and "Logical This Adjustment"

Consider next S::rvf(), which overrides R::rvf(). Most implementations note that S::rvf() must have a hidden this parameter of type S*. Since R’s rvf vftable slot may be used when this call occurs:

  
((R*)ps)->rvf(); // (*((R*)ps)->R::vfptr[1])((R*)ps)
  

Most implementations add another thunk to convert the R* passed to rvf into an S*. Some also add an additional vftable entry to the end of S’s vftable to provide a way to call ps->rvf() without first converting to an R*. MSC++ avoids this by intentionally compiling S::rvf() so as to expect a this pointer which addresses not the S object but rather the R embedded instance within the S. (We call this “giving overrides the same expected address point as in the class that first introduced this virtual function”.) This is all done transparently, by applying a “logical this adjustment” to all member fetches, conversions from this, and so on, that occur within the member function. (Just as with multiple inheritance member access, this adjustment is constant-folded into other member displacement address arithmetic.)

Of course, we have to compensate for this adjustment in our debugger.

  
ps->rvf(); // ((R*)ps)->rvf(); // S::rvf((R*)ps)
  

Thus MSC++ generally avoids creating a thunk and an additional extra vftable entry when overriding virtual functions of non-leftmost bases.

Adjuster Thunks

As described, an adjuster thunk is sometimes called for, to adjust this (which is found just below the return address on the stack, or in a register) by some constant displacement en route to the called virtual function. Some implementations (especially cfront-based ones) do not employ adjuster thunks. Rather, they add additional displacement fields to each virtual function table entry. Whenever a virtual function is called, the displacement field, which is quite often 0, is added to the object address as it is passed in to become the this pointer:

  
ps->rvf();
// struct { void (*pfn)(void*); size_t disp; };
// (*ps->vfptr[i].pfn)(ps + ps->vfptr[i].disp);
  

The disadvantages of this approach include both larger vftables and larger code sequences to call virtual functions.

More modern PC-based implementations use adjust-and-jump techniques:

  
S::pvf-adjust: // MSC++
   this -= SdPR;
   goto S::pvf()
  

Of course, the following code sequence is even better (but no current implementation generates it):

  
S::pvf-adjust:
   this -= SdPR; // fall into S::pvf()
S::pvf() { ... }
  

Virtual Functions: Virtual Inheritance

Here T virtually inherits P and overrides some of its member functions. In Visual C++, to avoid costly conversions to the virtual base P when fetching a vftable entry, new virtual functions of T receive entries in a new vftable, requiring a new vfptr, introduced at the top of T.

  
struct T : virtual P {
   int t1;
   void pvf();         // overrides P::pvf
   virtual void tvf(); // new
};
  

  
void T::pvf() {
   ++p1; // ((P*)this)->p1++; // vbtable lookup!
   ++t1; // this->t1++;
}
  

As shown above, even within the definition of a virtual function, access to data members of a virtual base must still use the vbtable to fetch a displacement to the virtual base. This is necessary because the virtual function can be subsequently inherited by a further derived class with different layout with respect to virtual base placement. And here is just such a class:

  
struct U : T {
   int u1;
};
  

Here U adds another data member, which changes the dP, the displacement to P. Since T::pvf expects to be called with a P* in a T, an adjuster thunk is necessary to adjust this so it arrives at the callee addressing just past T::t1 (the address point of a P* in a T). (Whew! That’s about as complex as things get!)

Special Member Functions

This section examines hidden code compiled into (or around) your special member functions.

Constructors and Destructors

As we have seen, sometimes there are hidden members that need to be initialized during construction and destruction. Worst case, a constructor may perform these activities

  • If “most-derived,” initialize vbptr field(s) and call constructors of virtual bases.
  • Call constructors of direct non-virtual base classes.
  • Call constructors of data members.
  • Initialize vfptr field(s).
  • Perform user-specified initialization code in body of constructor definition.

(A “most-derived” instance is an instance that is not an embedded base instance within some other derived class.)

So, if you have a deep inheritance hierarchy, even a single inheritance one, construction of an object may require many successive initializations of a class’s vfptr. (Where appropriate, Visual C++ will optimize away these redundant stores.)

Conversely, a destructor must tear down the object in the exact reverse order to how it was initialized:

  • Initialize vfptr field(s).
  • Perform user-specified destruction code in body of destructor definition.
  • Call destructors of data members (in reverse order).
  • Call destructors of direct non-virtual bases (in reverse order).
  • If “most-derived,” call destructors of virtual bases (in reverse order).

In Visual C++, constructors for classes with virtual bases receive a hidden “most-derived flag” to indicate whether or not virtual bases should be initialized. For destructors, we use a “layered destructor model,” so that one (hidden) destructor function is synthesized and called to destroy a class including its virtual bases (a “most-derived” instance) and another to destroy a class excluding its virtual bases. The former calls the latter, then destroys virtual bases (in reverse order).

Virtual Destructors and Operator Delete

Consider structs V and W.

  
struct V {
  
  
   virtual ~V();
};
  

  
struct W : V {
   operator delete();
};
  

Destructors can be virtual. A class with a virtual destructor receives a hidden vfptr member, as usual, which addresses a vftable. The table contains an entry holding the address of the virtual destructor function appropriate for the class. What is special about virtual destructors is they are implicitly invoked when an instance of a class is deleted. The call site (delete site) does not know what the dynamic type being destroyed is, and yet it must invoke the appropriate operator delete for that type. For instance, when pv below addresses a W, after W::~W() is called, its storage must be destroyed using W::operator delete().

  
V* pv = new V;
delete pv;   // pv->~V::V(); // use ::operator delete()
pv = new W;
delete pv;   // pv->~W::W(); // use W::operator delete()
pv = new W;
::delete pv; // pv->~W::W(); // use ::operator delete()
  

To implement these semantics, Visual C++ extends its “layered destructor model” to automatically create another hidden destructor helper function, the “deleting destructor,” whose address replaces that of the “real” virtual destructor in the virtual function table. This function calls the destructor appropriate for the class, then optionally invokes the appropriate operator delete for the class.

Arrays

Dynamic (heap allocated) arrays further complicate the responsibilities of a virtual destructor. There are two sources of complexity. First, the dynamic size of a heap allocated array must be stored along with the array itself, so dynamically allocated arrays automatically allocate extra storage to hold the number of array elements. The other complication occurs because a derived class may be larger than a base class, yet it is imperative that an array delete correctly destroy each array element, even in contexts where the array size is not evident:

  
struct WW : W { int w1; };
pv = new W[m];
delete [] pv; // delete m W's  (sizeof(W)  == sizeof(V))
pv = new WW[n];
delete [] pv; // delete n WW's (sizeof(WW) >  sizeof(V))
  

Although, strictly speaking, polymorphic array delete is undefined behavior, we had several customer requests to implement it anyway. Therefore, in MSC++, this is implemented by yet another synthesized virtual destructor helper function, the so-called “vector delete destructor,” which (since it is customized for a particular class, such as WW) has no difficulty iterating through the array elements (in reverse order), calling the appropriate destructor for each.

Exception Handling

Briefly, the exception handling proposal in the C++ standards committee working papers provides a facility by which a function can notify its callers of an exceptional condition and select appropriate code to deal with the situation. This provides an alternative mechanism to the conventional method of checking error status return codes at every function call return site.

Since C++ is object-oriented, it should come as no surprise that objects are employed to represent the exception state, and that the appropriate exception handler is selected based upon the static or dynamic type of exception object “thrown.” Also, since C++ always ensures that frame objects that are going out of scope are properly destroyed, implementations must ensure that in transferring control (unwinding the stack frame) from throw site to “catch” site, (automatic) frame objects are properly destroyed.

Consider this example:

  
struct X { X(); }; // exception object class
struct Z { Z(); ~Z(); }; // class with a destructor
extern void recover(const X&);
void f(int), g(int);

int main() {
   try {
      f(0);
   } catch (const X& rx) {
      recover(rx);
   }
   return 0;
}

void f(int i) {
   Z z1;
   g(i);
   Z z2;
   g(i-1);
}

void g(int j) {
   if (j < 0)
      throw X();
}
  

This program will throw an exception. main() establishes an exception handler context for its call to f(0), which in turn constructs z1, calls g(0), constructs z2, and calls g(-1). g() detects the negative argument condition and throws an X object exception to whatever caller can handle it. Since neither g() nor f() established an exception handler context, we consider whether the exception handler established by main() can handle an X object exception. Indeed it can. Before control is transferred to the catch clause in main(), however, objects on the frame between the throw site in g() and the catch site in main() must be destroyed. In this case, z2 and z1 are therefore destroyed.

An exception handling implementation might employ tables at the throw site and the catch site to describe the set of types that might catch the thrown object (in general) and can catch the thrown object at this specific catch site, respectively, and generally, how the thrown object should initialize the catch clause “actual parameter.” Reasonable encoding choices can ensure that these tables do not occupy too much space.

However, let us reconsider function f(). It looks innocuous enough. Certainly it contains neither try, catch, nor throw keywords, so exception handling would not appear to have much of an impact on f(). Wrong! The compiler must ensure that, once z1 is constructed, if any subsequently called function were to raise an exception (“throw”) back to f(), and therefore out of f(), that the z1 object is properly destroyed. Similarly, once z2 is constructed, it must ensure that a subsequent throw is sure to destroy z2 and then z1.

To implement these “unwind semantics,” an implementation must, behind the scenes, provide a mechanism to dynamically determine the context (site), in a caller function, of the call that is raising the exception. This can involve additional code in each function prolog and epilog, and, worse, updates of state variables between each set of object initializations. For instance, in the example above, the context in which z1 should be destroyed is clearly distinct from the subsequent context in which z2 and then z1 should be destroyed, and therefore Visual C++ updates (stores) a new value in a state variable after construction of z1 and again after construction of z2.

All these tables, function prologs, epilogs, and state variable updates, can make exception handling functionality a significant space and speed expense. As we have seen, this expense is incurred even in functions that do not employ exception handling constructs.

Fortunately, some compilers provide a compilation switch and other mechanisms to disable exception handling and its overhead from code that does not require it.

Summary

There, now go write your own compiler.

Seriously, we have considered many of the significant C++ run-time implementation issues. We see that some wonderful language features are almost free, and others can incur significant overhead. These implementation mechanisms are applied quietly for you, behind the curtains, so to speak, and it is often hard to tell what a piece of code costs when looking at it in isolation. The frugal coder is well advised to study the generated native code from time to time and question whether use of this or that particularly cool language feature is worth its overhead.

Acknowledgments. The Microsoft C++ Object Model described herein was originally designed by Martin O’Riordan and David Jones; yours truly added details here and there as necessary to complete the implementation.

-------------------------------

WARRANTY DISCLAIMER

THESE MATERIALS ARE PROVIDED "AS-IS", FOR INFORMATIONAL PURPOSES ONLY. NEITHER MICROSOFT NOR ITS SUPPLIERS MAKES ANY WARRANTY, EXPRESS OR IMPLIED, WITH RESPECT TO THE CONTENT OF THESE MATERIALS OR THE ACCURACY OF ANY INFORMATION CONTAINED HEREIN, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. BECAUSE SOME STATES/JURISDICTIONS DO NOT ALLOW EXCLUSIONS OF IMPLIED WARRANTIES, THE ABOVE LIMITATION MAY NOT APPLY TO YOU.

NEITHER MICROSOFT NOR ITS SUPPLIERS SHALL HAVE ANY LIABILITY FOR ANY DAMAGES WHATSOEVER, INCLUDING CONSEQUENTIAL INCIDENTAL, DIRECT, INDIRECT, SPECIAL, AND LOSS PROFITS. BECAUSE SOME STATES/JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF CONSEQUENTIAL OR INCIDENTAL DAMAGES, THE ABOVE LIMITATION MAY NOT APPLY TO YOU. IN ANY EVENT, MICROSOFT'S AND ITS SUPPLIERS' ENTIRE LIABILITY IN ANY MANNER ARISING OUT OF THESE MATERIALS, WHETHER BY TORT, CONTRACT, OR OTHERWISE, SHALL NOT EXCEED THE SUGGESTED RETAIL PRICE OF THESE MATERIALS.

 

你可能感兴趣的:(C++,exception,function,struct,inheritance,destructor)