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

来源:互联网 发布:java开发项目经验描述 编辑:程序博客网 时间:2024/05/14 04:30

 ***非静态成员函数(Nonstatic Member Functions)***

C++的设计准则之一就是:nonstatic member function至少必须和一般的nonmember function有相同的效率。也就是说,如果我们要在以下两个函数之间作选择:

float magnitude3d(const Point3d *this{ ... }
float Point3d::magnitude3d() const { ... }

那么选择member function不应该带来什么额外负担。因为编译器内部已将“member函数实体”转化为对等的“nonmember函数实体”。下面是magnitude()的一个nonmember定义:

float Pointer3d::magnitude() const
{
return sqrt(_x*_x + _y*_y + _z*_z);
}

// 内部转化为
float magnitude_7Point3dFv(const Point3d *this)  //已对函数名称进行“mangling”处理
{
return sqrt(this->_x*this->_x + this->_y*this->_y + this->_z*this->_z);
}

现在,对该函数的每一个调用操作也都必须转换:

obj.magnitude();
// 转换为
magnitude_7Point3dFv(&obj);

对于class中的memeber,只需在member的名称中加上class名称,即可形成独一无二的命名。但由于member function可以被重载化,所以需要更广泛的mangling手法,以提供绝对独一无二的名称。其中一种做法就是将它们的参数链表中各参数的类型也编码进去。

class Point {
public:
void x(float newX);
float x();
...
}
;
// 内部转化为
class Point {
void x_5PointFf(float newX);  // F表示function,f表示其第一个参数类型是float
float x_5PointFv();  // v表示其没有参数
}
;

上述的mangling手法可在链接时期检查出任何不正确的调用操作,但由于编码时未考虑返回类型,故如果返回类型声明错误,就无法检查出来。

 

***虚拟成员函数(Virtual Member Functions)***

对于那些不支持多态的对象,经由一个class object调用一个virtual function,这种操作应该总是被编译器像对待一般的nonstatic member function一样地加以决议:

// Point3d obj
obj.normalize();
// 不会转化为
(*obj.vptr[1])(&obj);
// 而会被转化未
normalize_7Point3dFv(&obj);

 

***静态成员函数(Static Member Functions)***

在引入static member functions之前,C++要求所有的member functions都必须经由该class的object来调用。而实际上,如果没有任何一个nonstatic data members被直接存取,事实上就没有必要通过一个class object来调用一个member function。
这样一来便产生了一个矛盾:一方面,将static data member声明为nonpublic是一种好的习惯,但这也要求其必须提供一个或多个member functions来存取该member;另一方面,虽然你可以不靠class object来存取一个static member,但其存取函数却得绑定于class object之上。

static member functions正是在这种情形下应运而生的。
编译器的开发者针对static member functions,分别从编译层面和语言层面对其进行了支持:
(1)编译层面:当class设计者希望支持“没有class object存在”的情况时,可把0强制转型为一个class指针,因而提供出一个this指针实体:

// 函数调用的内部转换
object_count((Point3d*)0);

(2)语言层面:static member function的最大特点是没有this指针,如果取一个static member function的地址,获得的将是其在内存中的位置,其地址类型并不是一个“指向class member function的指针”,而是一个“nonmember函数指针”:

unsigned int Point3d::object_count() return _object_count; }
&Point3d::object_count();
// 会得到一个地址,其类型不是
unsigned int (Point3d::*)();
// 而是
unsigned int (*)();

static member function经常被用作回调(callback)函数。

 

***虚拟成员函数(Virtual Member Functions)***

对于像ptr->z()的调用操作将需要ptr在执行期的某些相关信息,为了使得其能在执行期顺利高效地找到并调用z()的适当实体,我们考虑往对象中添加一些额外信息。
(1)一个字符串或数字,表示class的类型;
(2)一个指针,指向某表格,表格中带有程序的virtual functions的执行期地址;
在C++中,virtual functions可在编译时期获知,由于程序执行时,表格的大小和内容都不会改变,所以该表格的建构和存取皆可由编译器完全掌握,不需要执行期的任何介入。
(3)为了找到表格,每一个class object被安插上一个由编译器内部产生的指针,指向该表格;
(4)为了找到函数地址,每一个virtual function被指派一个表格索引值。
一个class只会有一个virtual table,其中内含其对应的class object中所有active virtual functions函数实体的地址,具体包括:

(a)这个class所定义的函数实体
它会改写一个可能存在的base class virtual function函数实体。若base class中不存在相应的函数,则会在derived class的virtual table增加相应的slot。
(b)继承自base class的函数实体
这是在derived class决定不改写virtual function时才会出现的情况。具体来说,base class中的函数实体的地址会被拷贝到derived class的virtual table相对应的slot之中。
(c)pure_virtual_called函数实体
对于这样的式子:

ptr->z();

运用了上述手法后,虽然我不知道哪一个z()函数实体会被调用,但却知道每一个z()函数都被放在slot 4(这里假设base class中z()是第四个声明的virtual function)。

// 内部转化为
(*ptr->vptr[4])(ptr);

 

***多重继承下的Virtual Functions***

在多重继承中支持virtual functions,其复杂度围绕在第二个及后继的base classes身上,以及“必须在执行期调整this指针”这一点。
多重继承到来的问题:
(1)经由指向“第二或后继之base class”的指针(或reference)来调用derived class virtual function,该调用操作连带的“必要的this指针调整”操作,必须在执行期完成;

以下面的继承体系为例:

class Base1 {
public:
Base1();
virtual ~Base1();
virtual void speakClearly();
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;
}
;
对于下面一行:
Base2 *pbase2 = new Derived;

 会被内部转化为:

// 转移以支持第二个base class
Derived *temp = new Derived;
Base2 
*pbase2 = temp ? temp + sizeof(Base1) : 0;

如果没有这样的调整,指针的任何“非多态运用”都将失败:

pbase2->data_Base2;

当程序员要删除pbase2所指的对象时:

// 必须调用正确的virtual destructor函数实体
// pbase2需要调整,以指出完整对象的起始点
delete pbase2;

指针必须被再一次调整,以求再一次指向Derived对象的起始处。然而上述的offset加法却不能够在编译时期直接设定,因为pbase2所指的真正对象只有在执行期才能确定。

自此,我们明白了在多重继承下所面临的独特问题:经由指向“第二或后继之base class”的指针(或reference)来调用derived class virtual function,该调用操作所连带的“必要的this指针调整”操作,必须在执行期完成。有两种方法来解决这个问题:
(a)将virtual table加大,每一个virtual table slot不再只是一个指针,而是一个聚合体,内含可能的offset以及地址。这样一来,virtual function的调用操作发生改变:
(*pbase2->vptr[1])(pbase2);
// 改变为
(*pbase2->vptr[1].faddr)(pbase2 + pbase2->vptr[1].offset);

这个做法的缺点是,它相当于连带处罚了所有的virtual function调用操作,不管它们是否需要offset的调整。

(b)利用所谓的thunk(一小段assembly码),其做了以下两方面工作:(1)以适当的offset值调整this指针;(2)跳到virtual function去。

pbase2_dtor_thunk:
this += sizeof(base1);
Derived::
~Derived(this);

Thunk技术允许virtual table slot继续内含一个简单的指针,slot中的地址可以直接指向virtual function,也可以指向一个相关的thunk。于是,对于那些不需要调整this指针的virtual function而言,也就不需要承载效率上的额外负担。

(2)由于两种不同的可能:(a)经由derived class(或第一个base class)调用;(b)经由第二个(或其后继)base class调用,同一函数在virtual table中可能需要多笔对应的slot;

Base1 *pbase1 = new Derived;
Base2 
*pbase2 = new Derived;

delete pbase1;
delete pbase2;

虽然两个delete操作导致相同的Derived destructor,但它们需要两个不同的virtual table slots:
(a)pbase1不需要调整this指针,其virtual table slot需放置真正的destructor地址
(b)pbase2需要调整this指针,其virtual table slot需要相关的thunk地址
具体的解决方法是:
在多重继承下,一个derived class内含n-1个额外的virtual tables,n表示其上一层base classes的数目。按此手法,Derived将内含以下两个tables:vtbl_Derived和vtbl_Base2_Derived。

(3)允许一个virtual function的返回值类型有所变化,可能是base type,可能是publicly derived type,这一点可以通过Derived::clone()函数实体来说明。

Base2 *pb1 = new Derived;

// 调用Derived::clone()
// 返回值必须被调整,以指向Base2 subobject
Base2 *pb2 = pb1->clone();

当运行pb1->clone()时,pb1会被调整指向Derived对象的起始地址,于是clone()的Derived版会被调用:它会传回一个指针,指向一个新的Derived对象;该对象的地址在被指定给pb2之前,必须先经过调整,以指向Base2 subobject。
当函数被认为“足够小”的时候,Sun编译器会提供一个所谓的“split functions”技术:以相同算法产生出两个函数,其中第二个在返回之前,为指针加上必要的offset,于是无论通过Base1指针或Derived指针调用函数,都不需要调整返回值;而通过Base2指针所调用的,是另一个函数。

 

***虚拟继承下的Virtual Functions***

其内部机制实在太过诡异迷离,故在此略过。唯一的建议是:不要在一个virtual base class中声明nonstatic data members。

 

***函数的效能***

由于nonmember、static member和nonstatic member函数都被转化为完全相同的形式,故三者的效率安全相同。virtual member的效率明显低于前三者,其原因有两个方面:(a)构造函数中对vptr的设定操作;(b)偏移差值模型。

 

***指向Member Function的指针***

取一个nonstatic member function的地址,如果该函数是nonvirtual,则得到的结果是它在内存中真正的地址。
我们可以这样定义并初始化该指针:

double (Point::*coord)() = &Point::x;

想调用它,可以这么做:

(origin.*coord)();
    (ptr->*coord)();

“指向Virtual Member Functions”之指针将会带来新的问题,请注意下面的程序片段:

float (Point::*pmf)() = &Point::z;
Point 
*ptr = new Point3d;

其中,pmf是一个指向member function的指针,被设值为Point::z()(一个virtual function)的地址,ptr则被指向一个Point3d对象。
如果我们直接经由ptr调用z():

ptr->z();  // 调用的是Point3d::z()

但如果我们经由pmf间接调用z():

(ptr->*pmf)();  // 仍然调用的是Point3d::z()

也就是说,虚拟机制仍然能够在使用“指向member function之指针”的情况下运行,但问题是如何实现呢?
对一个nonstatic member function取其地址,将获得该函数在内存中的地址;而对一个virtual member function取其地址,所能获得的只是virtual function在其相关之virtual table中的索引值。因此通过pmf来调用z(),会被内部转化为以下形式:

(*ptr->vptr[(int)pmf])(ptr);
但是我们如何来判断传给pmf的函数指针指向的是内存地址还是virtual table中的索引值呢?例如以下两个函数都可指定给pmf:
// 二者都可以指定给pmf
float Point::x() return _x; }  // nonvirtual函数,代表内存地址
float Point::z() return 0; }  // virtual函数,代表virtual table中的索引值

cfront 2.0是通过判断该值的大小进行判断的(这种实现技巧必须假设继承体系中最多只有128个virtual functions)。
为了让指向member functions的指针也能够支持多重继承和虚拟继承,Stroustrup设计了下面一个结构体:

// 用以支持在多重继承之下指向member functions的指针
struct _mptr {
int delta;
int index;
union 
{
ptrtofunc faddr;
int v_offset;
}
;
}
;

其中,index表示virtual table索引,faddr表示nonvirtual member function地址(当index不指向virtual table时,被设为-1)。
在该模型之下,以下调用操作会被转化为:

(ptr->*pmf)();
// 内部转化为
(pmf.index < 0)
? (*pmf.faddr)(ptr)  // nonvirtual invocation
: (*ptr->vptr[pmf.index](ptr)  // virtual invocation

对于如下的函数调用:

(pA.*pmf)(pB);  // pA、pB均是Point3d对象

会被转化成:

pmf.iindex < 0
? (*pmf.faddr)(&pA + pmf.delta, pB)
: (
*pA._vptr_Point3d[pmf.index].faddr)(&pA + pA._vptr_Point3d[pmf.index] + delta, pB);

 

***Inline Functions***

在inline扩展期间,每一个形式参数都会被对应的实际参数取代。但是需要注意的是,这种取代并不是简单的一一取代(因为这将导致对于实际参数的多次求值操作),而通常都需要引入临时性对象。换句话说,如果实际参数是一个常量表达式,我们可以在替换之前先完成其求值操作;后继的inline替换,就可以把常量直接绑上去。
举个例子,假设我们有以下简单的inline函数:

inline int min(int i, int j)
{
return i < j ? i : j;
}

对于以下三个inline函数调用:

minval = min(val1,val2);
minval 
= min(1024,2048);
minval 
= min(foo(),bar()+1);

会分别被扩展为:

minval = val1 < val2 ? val1 : val2;  // 参数直接代换
minval = 1024;  // 代换之后,直接使用常量
int t1;
int t2;
minval 
= (t1 = foo()), (t2 = bar()+1),t1 < t2 ? t1 : t2;  //有副作用,所以导入临时对象

inline函数中的局部变量,也会导致大量临时性对象的产生。

inline int min(int i, int j)
{
int minval = i < j ? i : j;
return minval;
}

则以下表达式:

minval = min(val1, val2);

将被转化为:

int _min_lv_minval;
minval 
= (_min_lv_minval = val1 < val2 ? val1 : val2),_min_lv_minval;
总而言之,inline函数中的局部变量,再加上有副作用的参数,可能会导致大量临时性对象的产生。特别是如果它以单一表达式被扩展多次的话。新的Derived对象的地址必须调整,以指向其Base2 subobject。
原创粉丝点击