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

 ***非静态成员函数(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。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值