Xander's Wiki


  • Home

  • Tags

  • Categories

  • Archives

  • Search

构造、析构、拷贝语义学——导读

Posted on 2018-05-17 | In Inside the C++ object model

问题实例

 
现有abstract base class声明如下:

1
2
3
4
5
6
7
8
class Abstract_base{
public:
virtual ~Abstract_base() = 0;
virtual void interface() const = 0;
virtual const char* mumble() const {return _mumble;}
protected:
char *_mumble;
};

我们可以看出,即使该class不会具象出一个实体,但它仍然需要一个明确的构造函数以初始化其data member _mumble。如果没有这个初始化操作,其derived class的局部性对象_mumble将无法决定初值。

可能会有人认为这是abstract_base的设计者试图让其每一个derived class提供_mumble初值,然而如果是这样,abstract_base必须提供一个带有唯一参数的protected constructor:

1
2
Abstract_base::
Abstract_base(char *mumble_value = 0):_mumble(mumble_value) {}

一般而言,class的data member应当在自身的constructor或其他member functions中被初始化,其他任何操作都将破坏封装性,使class的维护和修改变得更加困难。
当然,你也可以认为该设计并不合理,因为不应当将在abstract class中设定data member,这种思想体现了interface与implementation分离。但在base class中放置共有的data member也是行之有效的真理。


纯虚函数

 
在学习了长时间的C++后我们已经了解,即使是纯虚函数,也可以被定义和调用。只不过它们只能被静态地调用,无法通过虚拟机制调用,例如:

1
2
3
4
5
6
7
8
inline 
void Abstract::interface() const{//定义纯虚函数
...
}
inline
void Concrete_derived::interface() const{
Abstract_base::interface();//静态调用
}

具体是否需要定义和调用纯虚函数只看开发者是否需要,唯一的例外是pure virtual destructor 一定需要被定义。原因在于每一个derived class destructor会被编译器加以扩展,以静态调用的方式调用其“每一个virtual base class”以及“上一层base class”的destructor。在缺乏定义的情况下,会导致链接失败。


虚拟规格

 
把Abstract::base::mumble()设计为一个virtual function是不明智的,因为其函数定义内容并不与类型有关(并不因为class的类型而形成不同的操作),因而几乎不会被后继的derived class override。此外,由于它的non-virtual函数实体是一个inline函数,如果常常被调用,效率上恐怕会出现问题。
我们认为,无需对不必要声明为virtual的函数实行虚拟化,其弊端在Effective C++中亦有提及。


虚拟规格中const的存在

 
考虑virtual function的const属性很麻烦,因为我们也无法了解未来的需求。如果不把函数声明为const,则意味着该函数不能够获得一个const reference或const pointer,这似乎也不是什么缺点,所以我们提倡将所有的virtual function设置为const。


重新考虑class的声明

 
经过前面的讨论,我们将Abstarct_base重新定义如下:

1
2
3
4
5
6
7
8
9
class Abstract_base{
public:
virtual ~Abstract_base();//放弃纯虚
virtual void interface() = 0;//不再是const
const char* mumble() const {return _mumble;}//不再是virtual
protected:
Abstract_base(char *pc=0);
char *_mumble;
};

Function语义学——Inline Function

Posted on 2018-05-16 | In Inside the C++ object model

前言

 
众所周知,inline只是对编译器提出的申请,如果该请求被接受,则会使用某个expression合理地将该函数扩展开来。


处理inline函数

 
一般而言,处理一个inline函数有两个阶段:

  1. 分析函数定义以确定函数的intrinsic inline ability
    如果一个函数因为其复杂度或建构问题,被编译器判断为无法inline,则会被转为一个static函数,并在“被编译模块”内产生对应的函数定义。理想情况下链接器会将产生出来的重复代码清除,但实际情况视编译环境而定。
  2. 真正的inline函数扩展操作是在调用的哪一点上,这会带来参数求值操作及临时对象管理等问题

形式参数

 
在inline扩展期间,每一个形参都会被实参所取代,但这种取代操作并非想象中的一一替换,因为这会造成重复求值的可能。一般来说,为了避免这一可能性,我们会引入临时对象。举例而言,如果某个实参是一个常量表达式,我们可以在替换前先完成求值操作,后继需要该实参的地方则以计算值绑定。

问题实例

现有inline函数如下:

1
2
3
4
inline
int min(int i,int j){
return i<j?i:j;
}

现有三个调用操作:
1
2
3
4
5
6
7
8
9
inline
int bar(){
int minval;
int val1= 1024,val2 =2048;
minval = min(val1,val2); // expression 1
minval = min(1024,2048); // expression 2
minval = min(foo(),bar()+1);// expression 3
return minval;
}

三次调用的扩展情况如下:
1
2
minval = val1 < val2 ? val1 :val2; //expression 1
minval = 1024; //直接计算后完成绑定

expression1与expression2均不会产生任何副作用,而expression3需要导入一个临时对象,以避免重复求值:
1
2
3
int t1;
int t2;
minval= (t1=foo(),t2=bar()+1),t1<t2?t1:t2;


局部变量

 
如果我们的inline函数存在局部对象,如:

1
2
3
4
5
inline
int min(int i,int j){
int minval = i<j?i:j;
return minval;
}

我们的调用操作如下:
1
2
3
4
5
6
void bar() {
int local_var;
int minval;//直接展开会造成名称遮蔽
//...
minval = min(val1,val2);
}

inline被展开后的可能结果如下:
1
2
3
4
int local_var;
int minval;
int _min_lv_minval;//local var
minval = (_min_lv_minval=val1<val2?val1:val2),_min_lv_minval);

一般而言,inline函数中的每一个局部变量都必须被放在函数调用的一个封闭区段当中,拥有一个独一无二的名称。如果inline函数以单一表达式扩展多次,那么每一次扩展都需要自己的一组局部变量。如果inline函数以分离的多个式子被扩展多次,那么只需要一组局部变量,就可以重复使用。

inline函数中的局部变量,再加上有副作用的参数,可能会导致大量临时性对象的产生。特别是如果它以单一表达式被扩展多次的话。举例而言:

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

可能被扩展为:
1
2
3
4
5
6
7
int _min_lv_minval_00;
int _min_lv_minval_01;

int t1,t2;
minval =
((_min_lv_minval_00 = val1 < val2 ? val1:val2),_min_lv_minval_00)+
((_min_lv_minval_01 = (t1=foo()),(t2=foo()+1),t1 < t2 ? t1:t2),_min_lv_minval_01);

inline函数对于封装提供了一种必要的支持,可以有效存取封装于class中的nonpublic数据,同时也是C程序宏定义的有效替代物。

一如我所描述过的,参数带有副作用,又或者以一个单一表达式做多重调用,或在inline函数中存在多个局部变量,都会产生临时性对象。编译器可能无法对它们执行优化操作。另外,inline中再次含有inline可能会形成低效的串联(可能会发生于复杂class体系下的constructors)。inline函数提供了一个强而有力的工具,然而,与non-inline函数比起来,它们需要更加小心地处理。

Function语义学——指向Member Function的指针

Posted on 2018-05-16 | In Inside the C++ object model

前言

 
在第三章中我们已经看出,取一个nonstatic data member的地址,得到的结果是该member在class布局中的bytes位置(+1)。我们可以想象它是一个不完整的值,必须依附于某个class object才可以完成存取。


Nostatic Nonvirtual Member Function的指针

 
取一个nonstatic nonvirtual member function的地址,得到的是该函数在内存中真正的地址,但这个值也是不完全的,它也需要被绑定到某个class object的地址上才能调用该函数。所有nonstatic member function都需要对象的地址(以参数this指出)。

一个指向member function的指针其声明语法如下:

1
2
3
4
double        //return type
( Point::* //class the function is member
pmf) //name of the pointer
() //argument list

member function指针使用如下:
1
2
3
4
double (Point::*coord)() = &Point::x;//定义、初始化
coord = &Point::y;//赋值
(origin.*coord)();//调用 被编译器转为(coord)(&origin)
(ptr->*coord)();//调用 被编译器转为(coord)(ptr)

指向member function的指针的声明语法,以及指向“member selection运算符”的指针,其作用是作为this指针的空间保留者。这也是static member functions的类型是函数指针,而非“指向member function的指针”的原因。

我们可以认为,使用“指向member function的指针”,其成本并不高于使用一个“nonmember function指针”,这句话的前提是,当前并没有发生virtual function、多重继承、虚继承。


支持“指向Virtual Member Functions的指针”

 
考虑以下程序片段:

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

显然,pmf指向了Point的一个member function,只不过这一次z()是一个virtual function。ptr指向了一个Point3d对象。
接下来我们执行调用操作:
1
2
ptr->z();//正确地调用Point3d::z()
(ptr->*pmf)();//仍然具备正确性

Poninter to virtual Member Function的实现

 
在上文中我们已经明确,对一个nonstatic member function取址得到的是其在内存中的地址。但对于一个virtual function,其地址在编译期是未知的,我们所知道的只是virtual function在其相关vtbl中的索引值。因此,对一个virtual member function取址的时候,我们得到的是一个索引。

问题实例

现有Point声明如下:

1
2
3
4
5
6
7
8
class Point{
public:
virtual ~Point();
float x();
float y();
virtual float z();
...
};

现对各个member func分别取址:
1
2
3
4
&Point::~Point();//res==1;
&Point::x();//内存中地址
&Point::y();//内存中地址
&Point::z()//res==2

通过pmf来调用z()的话,编译器会将该调用语句转为:
1
(*ptr->vptr[(int)pmf])(ptr);

显然地,pmf需要有两种含义:

  1. 内存地址
  2. vtbl中的索引值

关键在于我们应当如何区分它们。

解决方案

在cfront编译器中,具体解决方案如下:

1
2
3
(((int)pmf)&~127)?
(*pmf)(ptr):
(*ptr->vptr[(int)pmf](ptr));

这种设计策略要求继承体系中最多只能存在128个virtual functions。由于多重继承的导入,我们需要更一般化的实现方式,并趁机去除virtual functions的数目限制。


多重继承下,指向member function的指针

 
为了保证支持多重继承与虚继承,cfront的作者设计了一个结构体:

1
2
3
4
5
6
7
8
struct _mptr{
int delta;
int index;
union{
ptrtofunc faddr;
int v_offset;
};
};

index与faddr分别(不同时)带有vbtl索引与nonvirtual member function地址。在该模型下,调用操作
1
(ptr->*pmf)();

会变成:
1
2
3
(pmf.index<0)
?(*pmf.faddr)(ptr)//non-virtual
:(*ptr->vptr[pmf.index](ptr));

这种方法有两个缺点:

  1. 每一个调用操作都必须付出上述成本,检查其是否为virtual或non-virtual。
  2. 当传递一个不变值的指针给member function时,它需要产生一个临时对象。
    1
    2
    3
    4
    5
    extern Point3d foo(const Point3d&,Point3d(Point3d::*)());
    void bar(const Point3d& p){
    Point3d pt = foo(p,&Point3d::normal);
    ...
    }
    假设,&Point3d::normal的值类似于:{0,-1,10727417},那么将会产生一个具有明确初值的临时对象:
    1
    2
    _mptr temp={0,-1,10727417};
    foo(p,temp);

现在我们将目光再次拉回结构体。delta字段表示this指针的offset值,而v_offset字段存放的则是一个virtual base class的vptr位置。显然,如果vptr被置于class object的起始处,我们就可以不需要这个字段(当然这令我们丧失了与C语言的兼容性)。这些字段只有在多重继承或者虚拟继承的情况下才具备必要性。


“指向Member Functions的指针”的效率

 
(下文详细测试了不同指针的读取效率。)

Function语义学——函数的效能

Posted on 2018-05-16 | In Inside the C++ object model

(本节讨论了三种函数在优化与未优化之后的结果,它们一般都被转化成为了nonmember形式,具备同样的效率)

Function语义学——Virtual Member Function

Posted on 2018-05-15 | In Inside the C++ object model

前言

 
我们已经初步了解了virtual function的实现原理:vtbl与vptr。在本节中,我们将根据一个实例,分析单一继承、多重继承、虚拟继承等各种情况,从细节上探究virtual function模型。


运行期类型判断

 
为了完成virtual function机制,我们能够在执行期判断出当前pointer或reference的动态类型,如此才能找到并调用相应的函数实体。

方法一

考虑下述语句:

1
ptr->z();

最直接了当的做法就是把必要的信息加上ptr身上,如此一来,pointer或者reference至少包含两个信息:

  1. 内容(它所指向的object的地址)
  2. 对象的动态类型

这个方法有两个缺点:

  1. 增加了空间负担,即使程序并不体现出多态性
  2. 破坏了与C的兼容性

方法二

如果不能在pointer或erference中加入信息,那么势必只能在对象中添加信息。显然并非所有的对象都应该被加入信息,我们应该在“可能会运行期多态”的class中加入RTTI信息。识别一个class是否支持多态,只需要去看他是否具备任何virtual function即可。


单一继承下virtual function的实现

 
在明确了需要有信息插入class object之后,我们需要明确:哪些信息需要被插入class?仍以下述语句为例:

1
ptr->z();

我们需要了解:

  1. ptr的动态类型,以方便调用正确的z()实体。
  2. z()实体位置

落实到具体实现,我们需要在每一个具备多态的class object身上增加两个members:

  1. 一个用以表示动态类型的字符串或者数字
  2. 一个指向某个table的指针,该table内部存放着virtual functions的执行期地址

上述工作均由编译器完成,执行期只是按图索骥而已。

一个class只会有一个vtbl,每一个vtbl内部含有所有virtual functions函数实体的地址,这些virtual functions包括:

  • 该class定义的函数实体,它可能会overriding一个可能存在的base class virtual function函数实体。
  • 继承自base class的函数实体,这是在derived class不覆写时发生的情况。
  • 一个pure_virtual_called函数实体,它既可以扮演pure virtual function的空间保卫者,也可以当做执行期异常处理函数。

具体实例

考虑下述定义:

1
2
3
4
5
6
7
8
9
class Point{
public:
virtual ~Point();
virtual Point& mult(float)=0;
float x() const;
virtual float y() const;
virtual float z() const;
...
}

现有Point2d derived from Point:
1
2
3
4
5
6
class Point2d:public Point{
public:
...
Point2d& mult(float);//override
float y() const;//override
}

并有Point3d derived from Point2d:
1
2
3
4
5
class Point3d:public Point2d{
...
Point2d& mult(float);//override
float y() const;//override
}

其具体的vtbl如下所示:
image_1cdha351o116i11o21cr67nojva9.png-369.9kB
当一个class派生自另一个class,一共具备三种可能性:

  1. 继承声明与实现
    在该情况下virtual functions的地址将会被拷贝到derived class的vtbl中,且保持原有的slot。
  2. 只继承声明
    此时函数实体的地址被置于对应的slot中。
  3. 增加新的virtual function
    此时derived class的vtbl会被扩充,新的virtual function会对应于新的slot。

当我们遇到

1
ptr->z();

如何完成运行期virtual function的调用呢?

  1. 尽管我们并不了解ptr的动态类型,但我们了解其vptr的位置,并由vptr找到vtbl
  2. 尽管并不明确哪一个z()实体会被调用,但我们明确了解z()必然位于slot4

因此,上述语句被转化为:

1
(*ptr->vptr[4])(ptr);

尽管该语句的效果在执行期才能被确定(调用哪个z实体),但至少在编译期我们完成了正确的转化。

在单一继承体系中,virtual function运转良好,但在多继承和虚继承中实际情况却较为麻烦。


多重继承下的virtual function

 
多重继承下支持virtual function的难点在于后继base class,以及在执行期调整this指针。
以下述继承体系为例:
image_1cdhf6elj1btn1ocfv1n19tgjucm.png-15.9kB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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;
};

Derived支持virtual function的难点统统落在Base2 subobject上,其中大致有三个问题:

  1. virtual destructor
  2. 被继承的Base2::mumble()
  3. 一组clone()函数实体

解决方案

首先,我们将一个从heap中配置得到的Derived对象的地址指定给一个Base2指针:

1
Base2 *pbase2 = new Derived;

新的Derived对象的地址必须调整,以指向Base2 subobject。在编译期间会有如下代码产生:
1
2
Derived *temp = new Derived;
Base2* pbase2 = temp?temp+sizeof(Base1):0;

当开发者需要释放pbase2所指向的对象时:
1
delete pbase2;

指针必须被再一次调整,以指向Derived对象的起始处。但在这里我们并不能像绑定操作那样在编译期完成offset添加,因为我们只有在运行期才能明确对象的真正类型。

如果我们经由类型为后继base class的指针或者引用来调用virtual function,该调用所连带的this指针调整操作必须在执行期完成,也就是说,编译器所做的关于扩充代码的操作,必须在某个地方插入。关键问题便是在哪里插入。

方案一

cfront编译器的做法是将vtbl增大,使它容纳此处所需要的this指针。每一个virtual table slot不再是一个指针,而是一个struct,内含可能的offset以及地址。于是调用操作由:

1
*(pbase2->vptr[1])(pbase2);

转变为:
1
*(pbase2->vptr[1].faddr)(pbase2+pbase2->vptr[1].offset)

这方法的缺点是增加了间接性,无论是否需要offset都需要去执行上述语句,并且vtbl的大小也扩大了。

方案二

比较有效率的解决方案是使用thunk。所谓thunk是一小段assembly码,用来完成以下两个任务:

  1. 以适当的offset调整this指针
  2. 跳转至virtual function

举例而言,经由一个Base2指针调用Derived destructor,其相关的thunk大致如下:

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

thunk技术保证了vtbl继续内含一个简单的指针,因此多重继承不再需要任何空间上的额外负担。slots中的地址可以指向virtual function,也可以指向一个相关的thunk(如果需要调整this指针)。

调整this指针的第二个额外负担是,函数的调用由于两种不同的可能:

  1. 经由derived class或第一base class调用
  2. 经由后继base class调用

同一函数需要在vtbl中存放多笔对应的slots,例如:

1
2
3
4
5
Base1* pbase1 = new Derived;
Base2* pbase2 = new Derived;

delete pbase1;
delete pbase2;

虽然两个delete操作导致相同的derived destructor,但它们需要两个不同的vtbl slots:

  1. pbase1不需要调整this指针,其vtbl放置的是真正的destructor地址
  2. pbase2需要调整指针,其vtbl slot需要放置相关的thunk地址。

在多重继承下,一个derived class内含n-1个额外的vtbl,n表示上一层base classes的数目。对于实例中的Derived,它会有两个vtbl:

  1. 一个主要实体,与Base1共享
  2. 一个次要实体,与Base2有关

针对每一个vtbl,derived对象中有相应的vptr。具体内存结构如下所示:
image_1cdhlsdfr1406v33m71ipd1pej13.png-379.6kB


后继base class对virtual function的影响

在三种情况下后继base class会影响对virtual function的支持:

  • 通过指向后继base class的指针来调用derived virtual function

    1
    2
    3
    Base2 *ptr = new Derived;
    //ptr必须向后调整sizeof(Base1)
    delete ptr;

    显然,ptr初始情况下指向了base2 subobject,为了能够正确执行,ptr必须调整指向Derived对象的起始处。

  • 通过一个指向derived class的指针调用继承自后继base class的virtual function

    1
    2
    3
    Derived *pder = new Derived;
    //pder必须向前调整sizeof(Base1)
    pder->mumble();

    此时,指针必须再次调整。

  • 返回值不相同

    具体来说就是clone的实现原理。


虚继承下的virtual functions

 
考虑下述继承体系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Point2d{
public:
...
virtual ~Point2d();
virtual void mumble();
virtual float z();
...
}

class Point3d:public virtual Point2d{
public:
~Point3d();
float z();
};

Point3d和Point2d的起始部分并不和“非虚拟的单一继承”一致。由于Point2d和Point3d的对象不再相符,两者之间的转换也就需要调整this指针。它们的内存布局:
image_1cdhu6f6t1uoo1qge1sc61avn1to1m.png-283.1kB
(这个图似乎有点问题,右下角发生了错误)

Function语义学——Member的各种调用方式

Posted on 2018-05-15 | In Inside the C++ object model

前言

 
最初的C with classes只支持nonstatic member functions。Virtual function在80年代中期被加入,static member function是1987年被最后加入。


Nonstatic Member Functions

 
C++的一大设计准则是:nonstatic member function至少需要与一般的nonmember function具备同样的效率。也就是说,假定现在有两个函数如下:

1
2
float magnitude3d(const Point3d *_this) {...}
float Point3d::magnitude3d() const {...}

这两个函数至少保证具备一致的调用效率,事实上,编译器总是会把member function转为对应的nonmember形式。

假设nonmember function定义式如下:

1
2
3
4
5
float magnitude3d(const Point3d *_this){
return sqrt(_this->_x*_this->_x+
_this->_y*_this->_y+
_this->_z*_this->_z);
}

member function的转化步骤

  • 改写函数signature
    具体来说,则是增加一个额外参数*this,保证class object得以调用。

    1
    float Point3d::magnitude(Point3d* const this);

    如果member function具备const属性,则函数被转化为

    1
    float Point3d::magnitude(const Point3d* const this);
  • 将data member的存取操作改为通过this指针完成:

    1
    2
    3
    return sqrt(_this->_x*_this->_x+
    _this->_y*_this->_y+
    _this->_z*_this->_z);
  • 将转化后的member function重写为一个外部函数,并对它执行名变化处理。

    1
    extern magnitude_7Point3dFv(register Point3d *const this);//独一无二的名称

现该函数已被转化完成,相对应的调用操作也需要执行转化:

1
2
3
4
5
6
//转化前
obj.magnitude();
ptr->magnitude();
//转化后
magnitude_7Point3dFv(&obj);
magnitude_7Point3dFv(ptr);

名变换

一般来说,member的名称前面需要加上class名称,形成独一无二的命名。举例而言:

1
class Bar {public:int ival;...};

其中的ival可能被名变换为ival_3Bar,编译器为什么需要对每一个member在名变换时考虑其class?
考虑下面的派生操作:
1
class Foo:public Bar {public:int ival;...};

在经过名变换后,Foo的内部描述如下所示:
1
2
3
4
5
6
class Foo{
public:
int ival_3Bar;
int ival_3Foo;
...
};

这样保证了在名变换后可以清楚地指出任何一个值。
Member functions可以被重载,因此其需要更广泛地名变换手法,例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//名变换前
class Point{
public:
void x(float newX);
float x();
...
}
//名变换后
class Point{
public:
void x_5PointFf(float newX);
float x_5PointFv();
...
}

这种将参数与函数名结合的名变换(函数名称+参数数目+参数类型)保证了:函数如果出现了错误的声明或调用形式,则无法发生正确的连接。但显然,它无法判断返回类型声明错误这种情况。


Virtual Member Functions

 
如果normalize()是一个virtual member function,那么以下的调用:

1
ptr->normalize();

将会被内部转化为:
1
(*ptr->vptr[1])(ptr);


Static Member Functions

 
nonstatic member functions之所以需要this指针,是因为其对某个具体的member进行存取。Static member function不需要存取具体的member,因此它也无需使用this指针。

Static member function具备三大特性:

  1. 不能够直接存取nonstatic member
  2. 不能被声明为const、volatile或virtual
  3. 无需经由class object被调用

由于缺乏this指针,static member function在内存布局中与nonmember function即为类似。

Function语义学——导读

Posted on 2018-05-14 | In Inside the C++ object model

假设我们有一个Point3d的指针与对象:

1
2
Point3d obj;
Point3d *ptr = &obj;

当我们试图去调用其成员函数:
1
2
obj.normalize();
ptr->normalize();

其中,Point::normalize()定义如下:
1
2
3
4
5
6
7
8
Point3d Point3d::normalize() const{
register float mag = magnitude();
Point3d normal;
normal._x=_x/mag;
normal._y=_y/mag;
normal._z=_z/mag;
return normal;
}

而Point3d::magnitude()有定义如下:
1
2
3
float Point3d::magnitude() const{
return sqrt(_x*_x+_y*_y+_z*_z);
}

在执行以上步骤时具体发生了什么?
答案是未知。C++支持三种类型的member functions:static、nonstatic、virtual,每一种类型被调用的方式都不相同,其具体差异将会在下一节详细描述。但在本节,我们尽管无法判断magnitude是否是virtual,但可以确定他一定不是static,有两个原因:

  1. 直接存取nonstatic数据
  2. 被声明为const

Data语义学——指向Data Members的指针

Posted on 2018-05-14 | In Inside the C++ object model

前言

 
通过指向Data Members的指针,我们可以更加细致地探究对象布局,比如说了解vptr是在顶端还是底端,又或者说了解class中access sections的次序。


问题实例

 
考虑下面的Point3d,其内含一个virtual function,一个static member,以及三个坐标值:

1
2
3
4
5
6
7
8
class Point3d{
public:
virtual ~Point3d();
...
private:
static Point3d origin;
float x,y,z;
};

该class object内部会含有vptr,我们不能确定的是其位于头部还是尾部。

在一个32位的机器上,一个float是4bytes,一个vptr也是4bytes。因此,三个坐标值在对象中的偏移量应该是0,4,8(vptr位于尾部)或4,8,12(位于头部)。但如果你去取data members的地址,传回的值会总是多1,也就是1,5,9或5,9,13。下面将详细描述原因。


问题剖析

 
如何区分一个“没有指向任何data member”的指针和一个“指向第一个data member的指针”?考虑如下例子:

1
2
3
4
5
6
float Point3d::*p1 = 0;//Point3d::*的意思在于“指向Point3d data member”的指针类型
float Point3d::*p2 = &Point3d::x;

if(p1==p2){//如何区分p1 p2?
cout << "contain same value" << endl;
}

为了区分p1与p2,每一个真正的member offset值都会被加1,因此编译器和开发者都应当明确,在真正使用该值以指出一个member之前,先减1。

真正Data地址与offset

在明确了“指向data members的指针”之后,我们发现解释& Point3d::z与& origin.z之间的差异则非常容易。
取一个nonstatic data member的地址将获得其在class中的offset,而取一个绑定于真正object身上的data,将会得到该member在内存中的真正地址。。那么我们执行

1
& origin.z - & Point3d::z

返回得到的就是origin的其实地址,但其类型是float *而非float Point3d::*。


多重继承

 
在多重继承下,如果需要将后继base class的指针和一个“derived class object data member”相结合,会由于需要加入offset而变得相当复杂,举例而言:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Base1 {int val1;};
struct Base2 {int val2;};
struct Derived:Base1,Base2 {...};
void func1(int derived::*dmp,Derived *pd){
//期待的是一个指向derived的data member指针
//但实际传入的可能是一个指向base class的data member的指针
pd->*dmp;
}
void func2(Derived *pd){
//bmp==1,但在derived中,val2==5
int Base2::*bmp = &Base2::val2;
func1(bmp,pd);
}

当bmp被作为func1的第一个参数时,其值必须受到调整,否则pd->*dmp将存取到Base::val1,而非开发者所以为的Base::val2。为了解决这个问题,必须由编译器转为:
1
2
//非null判定
func1(bmp?bmp+sizeof(Base1):0,pd);


“指向Members的指针”的效率问题

 
接下来作者以一系列数据比较了不同编译器优化下经由指针存取members的效率。

Data语义学——对象成员的效率

Posted on 2018-05-14 | In Inside the C++ object model

本节含有数个测试,旨在测试聚合、封装、继承所引发的额外负担。所有测试均以各别局部变量的加减、赋值等操作的存取成本为依据。我认为本节内容更偏重编译器优化,不同的编译器效果不同,有兴趣的读者可自行翻书。

Data语义学——继承与Data Member

Posted on 2018-05-13 | In Inside the C++ object model

前言

 
在C++继承模型中,derived class object所表现出来的东西,是其自身members与其base classes’s members的总和。它们出现的次序没有强制性约定,一般来说,base class members将先出现,但属于virtual base class的除外(virtual继承总能带来各种各样的意外)。

下面提出一个问题:假设我们单独为Point2d与Point3d建立class,而不是令他们派生自某个固有继承体系(如Inside the C++ object model 关于对象一章):

1
2
3
4
5
6
7
8
9
10
11
12
class Point3d{
public:
...
private:
float x,y;
};
class Point2d{
public:
...
private:
float x,y,z;
};

下述的各个小节将依次讨论不同情况下的区别。在没有virtual functions的情况下,我们可以认为上述两种写法的对象布局与C struct一致:
image_1cdbs9ibch0pqjd1og0ea7h5e9.png-26.9kB


Inheritance without Polymorphism

 
一般来说,C++中有将非尾端类设计为抽象类的准则,假设我们暂且忽略这一点,将Point3d derived from Point2d,从而保证共享数据和操作数据的方法。一般来说,具体继承不会增加空间或时间的开销,有继承体系如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Point2d{
public:
Point2d(float x=0.0,float y=0.0):_x(x),_y(y) {};
float x() {return _x;}
float y() {return _y;}
void x(float newX) {_x=newX;}
void y(float newY) {_y=newY;}
void operator+=(const Point2d& rhs){
_x+=rhs._x;
_y+=rhs._y;
}
protected:
float _x,_y;
};
class Point3d:public Point2d{
public:
Point3d(float x=0.0,float y=0.0,float z=0.0)
:Point2d(x,y),_z(z) {};
float z() {return _z;}
void z(float newZ) {_z=newZ;}
void operator+=(const Point3d& rhs){
Point2d::operator+=(rhs);
_z+=rhs.z();
}
protected:
float _z;
};

它们的对象模型如下所示:
image_1cdbu8p77snq1si1gvs19qs91sm.png-75.4kB

空间膨胀

将一个class分为多层,可能会因为“表现class体系的抽象化”而扩张所需空间。C++语言保证,出现在derived class中的base class subobject具备完整性,具体如下述实例所示:

1
2
3
4
5
6
7
8
9
10
class Concrete{
public:
...
private:
int val;
char c1;
char c2;
char c3;
char c4;
};

image_1cdbuglib14gi1vio1i3ccmf19gl13.png-22.9kB
图中左侧的数字表明在32位机器上,这一部分占用了多少bytes(padding表示alignment带来的影响)。
现假设出于某些需要,我们将该Concrete分为三层:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Concrete1{
public:
...
private:
int val;
char bit1;
};
class Concrete2:public Concrete1{
public:
...
private:
char bit2;
}
clss Concrete3:public Concrete2{
public:
...
private:
char bit3;
};

可能从设计的角度来说该结构比较合理,但我们会发现现在的Concrete3已经扩充到了16bytes,足足多了一倍,原因便是subobject的原样性必须保持。现在的对象布局如下所示:
image_1cdc03hof1afb1j9j14on1dil1bb1g.png-245kB

保证subobject一致性的原因

看起来很蠢,那么为什么我们非要保证base class subobject的原样性呢?
考虑下述指针:

1
2
Concrete2 *pc2;
Concrete1 *pc1_1,*pc1_2;//可以指向任意的concrete object

当我们通过指针来完成复制操作:
1
*pc1_2=*pc1_1;

会执行默认的memberwise复制,对象是被指的object的Concrete2或Concrete3 object。如果pc1_1实际上指向的是Concrete2或Concrete3 object,则上述操作实际上应该复制pc1_1指向对象的Concrete1 subobject部分。

如果,C++将derived class members与subobject捆绑(失去一致性),那么会直接导致复制操作出现各种未定义行为:
image_1cdc28lnr174m1jog1ij11uoiiun1t.png-430.4kB


Adding Polymorphism

 
如果我们在现有的继承体系中加入virtual function接口:

1
2
3
4
5
6
7
8
class Point2d{
public:
...
virtual void operator+=(const Point2d& rhs);
...
protected:
...
};

显然,virtual function仅仅发生于多态情况下:
1
2
3
4
5
void foo(Point2d& p1.Point2d& p2){
...
p1+=p2;
...
}

其中p1与p2既可能指向Point2d object也可能指向Point3d object。多态是面向对象程序设计的核心,支持这样的弹性势必需要给class带来空间与存取时间上的负担。

多态带来的开销

  1. vtbl
    vtbl将会被生成,其内部存放着所有virtual function的地址,显然,该table的大小与virtual function的数目正相关。内部可能还会有别的slots,用以支持RTTI;
  2. vptr
    每一个class object内部都被置入一个vptr,提供执行期链接,保证每一个object能够找到对应的vtbl。
  3. 加强constructor
    constructor需要为vptr设定初值,这可能意味着需要在derived class和每一个base class的constructor中重新设定vptr。
  4. 加强destructor
    destructor需要能够抹除指向vbtl的vptr,需要记住的是,destructor的调用次序是反向的:从derived class到base class。

vptr的位置

在设计C++编译器时的一个经典问题是:把vptr置于class object的哪一个位置最佳?

置于尾部

假设我们将其置于object的尾部:

1
2
3
4
5
6
7
8
9
10
11
struct no_virts{
int d1,d2;
}
class has_virts:public no_virts{
public:
virtual void foo();
//...
private:
int d3;
};
no_virts *p = new has_virts;

image_1cdcepq3a1kujvm18c68ci1isp9.png-78.1kB
把vptr置于class object的尾端可以保留base class C struct的对象布局,从而确保即使在C程序中也能使用。

置于头部

自C++2.0起,虚继承与抽象基类特性被引入,且OO兴起,部分编译器开始把vptr放在object的头部:image_1cdcfmt601mlv19nbvcla301euc13.png-77.9kB
将vptr置于前端,对于“在多重继承下,通过指向class members的指针调用member function”会带来帮助。但代价是丧失了与C struct的语言兼容性

下图展示了Point2d与Point3d加上了virtual function之后的内存布局(vptr位于尾端):
image_1cdcfslrde8v2vj191013rmn291g.png-82.2kB


Multiple Inheritance

 
在单一继承下,base class与derived class objects均从相同地址开始,只是其大小不同,

1
2
Point3d p3d;
Point2d* p = &p3d;

上述操作并不需要编译器去修改地址,且具备最佳执行效率。
上文曾叙述某些编译器将vptr置于class object的起始处。如果base class不具备virtual function而derived class具备的话,此时无法再保持对象起始地址的相同,需要编译器的介入以调整地址(因为vptr插入的缘故)。

多重继承不同于单一继承,也难以塑造模型,其复杂度在于derived class和其上一个base class乃至于上上个base class….之间的“非自然”关系,下文将以实例说明。

问题实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Point2d{
public:
...//具备virtual接口
protected:
float _x,_y;
};
class Point3d:public Point2d{
public:
...
protected:
float _z;
};
class Vertex{
public:
...//具备virtual接口
protected:
Vertex *p;
};
class Vertex3d:public Point3d,public Vertex{
public:
...
protected:
float mumble;
};

image_1cdci6k8ch0bm5b1rhh7f0lu81t.png-43.6kB
多重继承的主要问题在于derived class objects和其第二或后继的base class objects之间的转换,例如:

1
2
3
4
extern void mumble(const Vertex&);
Vertex3d v;
...
mumuble(v);//将v转化为Vertex对象

问题剖析

对于一个多重派生对象,将其地址指定给“最左端base class的指针”时,情况与单一继承情况一致。但如果你需要指定后继的base class地址,则需要编译器修改地址:加减介于中间的base class subobjects的大小:

1
2
3
4
Vertex3d v3d;
Vertex *pv;
Point2d *p2d;
Point3d *p3d;

对于上述声明,下面的赋值操作将引发内部地址转换如下:
1
2
pv = &v3d;
pv = (Vertex*)(((char*)&v3d)+sizeof(Point3d));

而下面的assignment则只需要简单地拷贝地址即可:
1
2
p2d = &v3d;
p3d = &v3d;

如果现有两个指针如下:
1
2
Vertex3d *pv3d;
Vertex *pv;

那么下面的assignment无法像上面那样简单地移位:
1
2
pv = pv3d;
pv = (Vertex*)(((char*)&pv3d)+sizeof(Point3d));

原因在于如果pv3d==0,那么pv将获得sizeof(Point3d),这不可取,因此需要修改为:
1
pv = pv3d?(Vertex*)(((char*)&pv3d)+sizeof(Point3d)):0;

引用在赋值时不需要考虑这么多,因为引用始终具备指向性(More Effective C++ 1)。下图给出了当前继承体系下的内存布局:
image_1cdcliqiiia8k7053914auc012a.png-298.9kB
存取后继base class中的data member无需付出额外的成本,因为members的位置在编译期间便已经固定。


Virtual Inheritance

 
多重继承的一大副作用在于,它必须支持某种形式的“shared subobject继承”。其中一个很典型的例子就是iostream library:

1
2
3
4
class ios {..};
class istream:public ios {...};
class ostream:public ios {...};
class iostream:public istream,public ostream {...};

显然,无论是istream还是ostream内部都含有一个ios subobject。我们希望避免在iostream中出现两个2个ios subobject,因此可以引入虚继承:
1
2
3
4
class ios {..};
class istream:public virtual ios {...};
class ostream:public virtual ios {...};
class iostream:public istream,public ostream {...};

image_1cdcn3a2gnqm15dlcje1q3j19tn2n.png-227.9kB

虚继承实现的难点

实现虚继承的一大难点在于需要找到一个足够有效的方法。将istream与ostream各自维护的一个ios subobject,折叠成为一个由iostream维护的单一ios subobject,并且保存pointer与reference对应的多态指定操作。

虚继承的具体实现

我们将class分为两部分:不变局部、共享局部。

  1. 不变局部
    不变局部中的数据不管后继如何衍化,总是拥有固定的offset,因此这一部分数据可以被直接存取。
  2. 共享局部
    所谓的共享局部则是virtual base class subobject这一部分的数据,其位置会由于派生操作而发生改变,所以它们只能被间接存取。

有三种主流策略来实现虚继承,我们将在实例中一一介绍。


问题实例

现有Vertex3d虚继承体系如下:
image_1cdcnn2kagir14on1lkg65rdd9.png-48.7kB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Point2d{
public:
...
protected:
float _x,_y;
};
class Point3d:public virtual Point2d{
public:
...
protected:
float _z;
};
class Vertex:public virtual Point2d{
public:
...
protected:
Vertex *p;
};
class Vertex3d:public Vertex,public Point3d{
public:
...
protected:
float mumble;
};

一般布局策略是先安置好不变局部,再建立其共享局部。关键在于如何去读取class的共享部分?

实现模型

部分编译器会在每一个derived class object中安插一些指针,每个指针指向一个virtual base class。存取继承得到的virtual base class members可以通过这些指针来完成,其对象布局大致如下:image_1cdcp8gvs28o10cvt9v19913f0m.png-340.9kB
举例而言,

1
2
3
4
5
6
7
8
9
10
//原有函数
void Point3d::operator+=(const Point3d &rhs){
_x+=rhs._x;
_y+=rhs._y;
_z+=rhs._z;
}
//真正实现
_vbcPoint2d->_x += rhs._vbcPoint2d->_x;//vbc means virtual base class
_vbcPoint2d->_y += rhs._vbcPoint2d->_y;
_z += rhs._z;

而一个derived class和一个base class的实例之间的转换如下所示:
1
2
Point2d *p2d = pv3d;
Point2d * p2d = pv3d?pv3d->__vbcPoint2d:0;

这种实现模型主要有两个缺点:

  1. 每一个virtual base class都存在一个与之对应的指针,但我们希望空间开销固定,不能因为virtual base class数目的增长而增长。
  2. 继承体系发生纵向增长后,间接存取的层次也将提高,我们希望时间开销固定,不随着继承体系扩张而增长。

第二个问题的解决方案是:拷贝所有的virtual base指针放在derived class object内部,保证一次间接访问即可获取data member,但这种方法增加了空间支出。

第一个问题有两种解决策略:

  1. virtual base class table
    Microsoft编译器采纳此法,如果一个class object存在一个或多个virtual base class,即会由编译器生成一个指向virtual base class table的指针,该table内部存放着访问各个virtual base class的指针。
  2. 在vbtl中放置virtual base class的offset
    其实现模型大致如下:image_1cdcqbki0avl6a816871u33f8g13.png-233.3kB
    vbtl由正值或负值来索引,正值则索引到virtual functions,负值则索引至virtual base class offsets。在该模型下,derived class实体与base class实体之间的转换操作如下所示:
    1
    2
    3
    Point2d *p2d = pv3d;
    //转换后的程序
    Point2d *p2d = pv3d? pv3d + pv3d->_vptr_Point3d[-1]:0;

最佳运用方式

一般而言,virtual base class最有效的一种运用方式是:一个抽象的virtual base class,没有任何data members(类似于Java的Interface)。

<i class="fa fa-angle-left"></i>1…111213…27<i class="fa fa-angle-right"></i>

xander.liu

266 posts
11 categories
36 tags
RSS
GitHub E-Mail
© 2024 xander.liu
Powered by Hexo
|
Theme — NexT.Pisces v5.1.4