Function语意学(The Semantics of Function)
1、Member的各种调用方式
- Nonstatic Member Functions(非静态成员函数)
C++的设计准则之一就是:非静态成员函数至少必须和一般的非成员函数有相同的效率,也就是说:float magnitude3d(const Point3d* _this){ }float Point3d::magnitude3d() const { }那么选择成员函数不应该带来额外的负担,因为编译器内部已将成员函数转换为对等的非成员函数实体。转换步骤:
- 改写函数原型,添加一个额外的参数,使class object得以调用该函数,这个额外的参数被称为this指针
- 将每一个对非静态数据成员的存取操作改为由this指针来存取
- 将成员函数重新写成一个外部函数,对函数进行“mangling”处理
- 名称的特殊处理(name mangling)
一般而言,member名称前面会被加上class名称,形成独一无二的命名,如:class Bar{ public: int ival; }//member经过name-mangling之后的可能结果ival_3Bar为什么要这么做?考虑派生操作class Foo : public Bar { public: int val;}//Foo内部class Foo{public:int ival_3Bar;int ival_3Foo;};不管你要处理哪一个ival,通过name-mangling,都可以绝对清楚的指出来。由于member functions可以被重载,所以需要更加广泛的mangling手法。为了让它们独一无二,唯有再加上它们的参数链表。( 但如果声明extern "C",就会压抑mangling效果)
- Virtual Member functions(虚拟成员函数)
如果normalize()是一个virtual member function,那么下面的调用:ptr->normalize();会被内部转化为:(*ptr->vptr[1])(ptr);
- vptr表示编译器产生的指针,指向virtual table,事实上,vptr也会被mangled,因为在一个复杂的class派生体系中,可能存在多个vptrs
- 1是virtual table slot的索引值,关联到normalize()函数
- 第二个ptr表示this指针
- Static Member Function(静态成员函数)
如果Point3d::normalize()是一个static member function,以下的两个调用操作:obj.normalize();ptr->normalize();将会被转换为一般的nonmember函数调用,类似:normalize_7Point3dSfv();normalize_7Point3dSfv();
static member fuctions的主要特性就是它没有members指针:
- 它不能够直接存取其class中的nonstatic members
- 它不能够被声明为const、volatile或virtual
- 它不需要经由class object才被调用
如果取一个static member function的地址,得到的是其在内存中的位置:&Point3d::object_count();会得到一个数值,类型是:unsigned int (*)();而不是 unsinged int (Point3d::*)();
2、Virtual Member Function(虚成员函数)
为了支持虚函数机制,必须能够对于多态对象有某种形式的“运行时类型判断法”,也就是说,ptr->z()需要在运行时期的某些相关信息,如此才能找到并调用z()适当的实体。或许最直截了当但成本最高的方法是把信息加在ptr身上,这样的策略下,一个指针(或者一个reference)含有两项信息:
- 它所参考到的对象的地址(也就是它当前所含有的东西);
- 对象类型的某种编码,或是某个结构(内含信息,以确定z()函数实例)的地址
此方法会带来两个问题,第一,明显增加了空间负担,即使不使用多态,;第二,它打断了与C程序的兼容性。在C++中,多态表示“以一个public base class 的指针或引用,寻址出一个derived class object”的意思,识别一个class是否支持多态,唯一适当的方法就是看它是否含有任何virtual function。什么信息才能让我们在执行期调用正确的z()实体?
- ptr所指对象的真实类型,这可以使我们选择正确的z()实体
- z()实体位置,以便我们调用它
一个class只会有一个virtual table,每一个table内含对应的class object中所有的active virtual function函数的地址,这些active virtual functions包括:
- 这个class所定义的函数实体,它会改写(overriding)一个可能存在的base class virtual function函数实体。
- 继承自base class的函数实体,这是在derived class决定不改写virtual function时的情况
- 一个pure_virtual_called()函数实体(这个是针对抽象类声明纯虚函数,因为没有实现,而添加到抽象类virtual table中的地址,当子类实现它时则存放的是具体纯虚函数地址),它既可以扮演pure virtual function的空间保卫者角色, 也可以当做运行时异常处理函数(有时候会用到)
当一个类派生自Point,一共会有三种可能:class Point { public: virtual ~Point(); virtual Point& mult(float) = 0; float x() const{ return _x;} virtual float y() const { return 0; } virtual float z() const { return 0; } protected: Point(float x = 0.0); float _x; }; virtual destructor被赋值slot1,而mult被赋值slot2,这里并没有mult的函数定义,他是一个pure virtual function,如果该函数被意外调用,通常会结束程序。y()被赋值slot3,z()被赋值slot4,x的slot是多少?答案是没有,因为想()不是virtual function。
现在,如果我们有ptr->z()这样的式子,那么我们如何有足够的知识在编译期设定virtual functions的调用呢?
- 它可以继承base class所声明的virtual functions的函数体,正确的说是该函数体的地址被拷贝到derived class的virtual table相对应的slot之中
- 它可以使用自己的函数体,这表示它自己的函数体地址必须放在对应的slot之中
- 它可以加入一个新的virtual function,此时virtual table的尺寸会增大一个slot,新的函数体地址会被放进该slot之中
以上的信息使得编译器可以将该调用转化为:
- 一般而言,我们不知道ptr所指对象的真正类型,然而我们知道,通过ptr可以存取到该对象的virtual table
- 虽然我们不知道哪一个z()函数会被调用,但我们知道一个z()函数地址被放在slot4
(*ptr->vptr[4]) (ptr);vptr表示编译器安插的指针,指向virtual table,4表示z()被赋值的slot编号。单继承体系中,virtual function机制的行为十分良好,不但有效率而且恨容易塑造出模型来,但是在多重继承和虚拟继承之中,就没有那么美好了。多重继承下的Virtual Functions
多重继承支持virtual functions的复杂度围绕在第二个及后继的base classed身上,以及“必须在运行时调整this指针”这一点。
考虑如下类体系
在多重继承下,一个derived class需要n-1个额外的virtual tables,n表示其上一层base classes的数目,因此,单一继承将不需要额外的virtual tables,对于本例的Derived 而言,会有两个virtual tables被编译器产生。class Base1 { public: Base1(); virtual ~Base1(); virtual void speekClearly(); virtual Base1 *clone() const(); protected: float data_Base1; }; class Base2 { public: Base2(); virtual ~Base2(); virtual void mumble(); virtual Base2 *clone() const(); protected: float data_Base2; }; class Derived : public Base1, public Base2 { public: Derived(); virtual ~Derived(); virtual Derived*clone() const; protected: float data_derived; }; “Derived支持virtual functions”的困难度,统统落在Base2 subobject身上,有三个问题需要解决,以此列而言是:(1)virtual destructor,(2)被继承下来的Base2::mumble(),(3)一组clone()函数体 首先,如下: Base2 *pbase2 = new Derived; 新的Derived对象的地址必须调整,以指向其Base2 subobject,编译期会产生如下代码: Derived *temp = new Derived; Base2 *pbase2 = temp ? temp + sizeof(Base1) : 0; 如果没有这样的调整,指针的任何非多态调用都将失败。 当程序员要删除pbase2所指对象时: //必须首先调整正确的virtual destructor //然后施行delete运算符 //pbase2可能需要调整,以指出完整对象的起始点 delete pbase2; 指针必须被再一次调整,以求再一次指向Derived对象的起始地址,然后上述的offset加法不能在编译时期直接设定,因为pbase2所指的对象只有在执行期才能确定,一般的规则是由指向“第二或后继的base class 指针”或reference来调用derived class virtual function: 比较有效率的解决办法是利用所谓的thunk。所谓Thunk是一小段assembly码,例如由一个Base2指针调用Derived destructor,相关的thunk看起来类似: pbase2_dtor_thunk: this += sizeof(base1); Derived::~Derived(this); Thunk技术允许virtual table slot继续包含一个简单指针,Slots中的地址可以直接指向virtual function,也可以指向一个相关的thunk(当需要调整this指针时)。 同一函数在virtual table中可能需要多笔对应的slots,例如: Base1 *pbase1 = new Derived: Base2 *pbase2 = new Derived: delete pbase1; delete pbase2; 虽然调用相同的Derived destructor,但他们需要两个不同的virtual table slots: 1.pbase1不需要调整this指针,因为它是最左端的base class,其virtual table slot放置真正的destructor地址 2.pbase2需要调整this指针,其virtual table slot需要相关thunk地址
针对每一个virtual tables,Derived对象中有对应的vptr。
多重继承有三种情况会影响对virtual function的支持,第一种就是通过一个指向第二个base class的指针,调用derived class virtual function,如:
Base2 *ptr = new Derived:
delete ptr;//调用Derived::~Derived
第二种情况是通过一个指向Derived class的指针,调用第二个base class中一个继承而来的virtual function,这种情况下Derived class指针必须再次调整,以指向第二个base subobject,如:
Derived *pder = new Derived;
pder->mumble();//pder必须被向前调整sizeof(Base1)个bytes
第三种情况发生在一个语言扩充性质下:允许一个virtual function的返回值类型有所变化,可能是Base type,也可能是publicly derived type。这一点可以通过Derived::clone()函数体来说明,当我们调用第二个base class的指针来调用clone()时,this指针的offset问题于是诞生了:
Base2 *pb1 = new Derived;
Base2 *pb2 = pb1->clone();//调用的是Derived::clone(),返回值必须被调整,以便指向Base2 subobject
虚拟继承下的virtual functions
有如下继承关系: class Point2d { public: Point2d(float = 0.0, float = 0.0); virtual ~Point2d(); virtual void mumble(); virtual float z(); protected: float _x, _y; }; class Point3d : public virtual Point2d { public: Point3d(float = 0.0, float = 0.0, float = 0.0); float z(); protected: float _z; }; 虽然Point3d只有唯一一个base class,但它们的起始部分并不像非虚拟的单一继承情况那样,由于Point2d,Point3d的对象不再相等,因为它们的转换也需要调整this指针。 当一个virtual base class从另一个virtual base class派生而来,并且两者都支持virtual functions和nonstatic data members时,编译器对于virtual base class的支持简直就像进了迷宫一样,我的建议是,不要在一个virtual base class中声明nonstatic data members,如果这么做,你会距离复杂的深渊越来越近
3、函数的效能
书中举例测试,其中有nonmember friend function,member function,virtual member function,并且virtual分别在单一、虚拟、多继承三种情况下测试:
nonmember 或static member或nonstatic member函数都被转化为完全相同的形式,效率完全相同,而未优化的inline函数提高了25%左右的效率,而优化版本的表现简直是奇迹,对virtual function的调用,效率降低了4%到11%不等,这是对于delta-offset(偏移差值)模型来支持virtual function的情况,而在thunck模型中,this指针的调整成本可以被局限在有必要那么做的函数中。多重继承中virtual function的调用似乎用掉较多成本。
注意:单一继承下,继承深度越多,构造函数复杂度也会增加,也会多消耗一些成本
4、指向Member Function的指针
此前我们已经看到过,取一个nonstatic data member的地址,得到的是它在class布局中的bytes位置(+1),它需要绑定到某个class object地址上,才能被存取。同样,取一个nonstatic member function的地址,如果是nonvirtual,则得到的是它在内存中真正的地址,然而这个值也是不完全的,它也要绑定到某个class object的地址上,才能够通过它调用改函数。一个指向member function的指针: double (Point::*pmf)(); 然后这样初始化该指针: double (Point::*coord)() = &Point::x; 也可以这样指定: coord = &Point::y; 调用它,可以这样: (origin.*coord)(); 或: (ptr->*coord)(); 这些操作会被转化为: (coord)(&origin); 和 (coord)(ptr); 使用一个member function指针,如果并不用于virtual function,多重继承、virtual base class等情况,与一个nonmember function指针,编译器可以提供相同的效率
支持“指向virtual member functions”的指针
( 太郁闷了真的,对于4这一点的笔记,上次保存前都是全部写完了的,但是这次继续打开写后面的东西发现没有了,不知道是不是csdn服务器没保存好还是bug了,这次就直接写下一点了,这里遗漏的就不补充了)如下程序片段: float (Point::*pmf)() = &Point::z; Point *ptr = new Point3d; pmf是一个指向member function的指针,被设置为Point::z()一个virtual function的地址,如果我们: ptr->z(); 被调用的是Point3d::z(); 如果我们: (ptr->*pmf)(); 仍然是Point3d:z()被调用吗?虚拟机制仍然能够在使用指向member fucntion的指针下运行吗?答案是yes
5、Inline Functions
我们并不能强迫将任何函数都变成inline,cfront有一套复杂的测试法,通常是用来计算assignments、function calls、virtual function calls等操作的次数,每个表达式种类有一个权值,而inline函数的复杂度就以这些操作的总和来决定。一般而言,处理一个inline函数,有两个阶段
大部分的厂商似乎认为不值得在inline支持技术上做详细的讨论,通常你必须进入到汇编器中才能看到是否真的实现了inline
- 分析函数定义,以决定函数的“intrinsic inline ability”。“intrinsic”意指“与编译器相关”。
- 真正的inline函数扩展操作是在调用的那一点上,这会带来参数的求值操作以及临时性对象的管理。
形式参数
inline扩展期间,每一个形式参数都被实际参数取代,如果说有什么副作用,那就是不可以只是简单地一一封塞程序中出现的每一个形式参数,因为这会导致实际参数的多次求值操作。如果实际参数是一个常量表达式,在替换之前先完成求值操作,如果不是常量表达式,也不是带有副作用的表达式,则直接替换之。
inline int min(int i, int j) { return i < j? i : j; } 下面三个调用操作: iline int bar() { int minVal; int val1 = 1024; int val2 = 2048; minval = min(val1, val2);//(1) minval = min(1024, 2048);//(2) minval = min(foo(), bar() + 1);//(3) return minval; } 标记为(1)那一行会被扩展为: minval = vla1 <val2 ? val1 : val2; 标记为2那一行直接使用常量: minval = 1024;//替换之后 标记为(3)那一行则引发参数的副作用,他需要引入一个临时对象,以避免重复求值: int t1; int t2; minval = (t1 = foo()), (t2 = bar()+1), t1 < t2 ? t1 : t2;
局部变量
在inline中定义一个局部变量,会怎样?inline函数的局部变量以“mangling”操作,以拥有独一无二的名称。inline函数中的局部变量,再加上有副作用的参数,可能会导致大量临时性对象的产生