第四章 Function语意学 (The Semantics of Function)
如果有一个Point3d的指针和对象:
Point3d obj; Point3d *ptr = &obj;
当这样做:
obj.normalize(); ptr->normalize();
时,会发生什么事情呢?其中的Point3d::normalize()定义如下:
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()定义如下:
float Point3d::magnitude() const { return sqrt(_x * _x + _y * _y + _z * _z); }
答案是:不知道.
C++支持三种类型的member functions:static,nonstatic,virtual,每一种类型被调用的方式都不相同.其间差异正是下一节的主题.不过,虽然不能够确定normalize()和magnitude()两函数是否为 virtual 或 nonvirtual,但可以确定它一定不是 static,原因有二:(1)它直接存取nonstatic数据;(2)它被声明为 const,static member functions不可能做到这两点.
4.1 Member的各种调用方式
回顾历史,原始的"C with Classes"只支持nonstatic member functions.Virtual 函数是在20世纪80年代中期被加进来的,并且受到许多质疑.Static member functions是最后被引入的一种函数类型,它们在1987年被正式加入C++中.
Nonstatic Member Functions (非静态成员函数)
C++的设计准则之一就是:nonstatic member functions至少必须和一般的nonmember function有相同的效率.也就是说,如果要在以下两个函数之间作选择:
float magnitude3d(const Point3d *_this) { ...} float Point3d::magnitude3d() const { ... }
那么选择member function不应该带来什么额外负担,这是因为编译器内部已将"member函数实体"转换为对等的"nonmember函数实体".
举个例子,下面是magnitude()的一个nonmember定义:
float magnitude3d(const Point3d *_this) { return sqrt(_this->_x * _this->_x + _this->_y * _this->_y + _this->_z * _this->_z); }
乍看之下似乎nonmember function比较没有效率,它间接地经由参数取用坐标成员,而member function确实直接取用坐标成员,然而实际上member function被内化为nonmember的形式,下面就是转化步骤:
1.
改写函数的signature(函数原型)以插入一个额外的参数到member function中,用以提供一个存取管道,使 class object得以调用该函数,该额外参数被称为 this 指针:
// non-const nonstatic member的扩张过程 Point3d Point3d::magnitude(Point3d *const this)
如果member function是const,则变成:
// const nonstatic member的扩张过程 Point3d Point3d::magnitude(const Point3d *const this)
2.将每一个"对 nonstatic data member的存取操作"改为经由 this 指针来存取:
{ return sqrt(this->_x * this->_x + this->_y * this->_y + this->_z * this->_z; }
3.将member function重新写成一个外部函数,对函数名称进行"mangling"处理,使它在程序中成为独一无二的语汇:
extern magnitude__7Point3dFV(register Point3d *const this);
现在这个函数已经被转换好了,而其每一个调用操作也必须转换.于是:
obj.magnitude();
变成了:
magnitude__7Point3dFV(&obj);
而
ptr->magnitude();
变成了:
magnitude__7Point3dFV(ptr);
本章一开始所提及的normalize()函数会被转化为下面的形式,其中假设已经声明有一个Point3d copy constructor,而named returned value(NRV)的优化也已施行:
// 以下描述"named return value函数"的内部转化 // 使用C++伪代码 void normalize_7Point3dFV(register const Point3d *const this, Point3d &__result) { register float mag = this->magnitude(); // default constructor __result.Point3d::Point3d(); __result._x = this->x / mag; __result._y = this->y / mag; __result._z = this->z / mag; return ; }
一个比较有效率的做法是直接建构"normal"值,像这样:
Point3d Point3d::normalize() const { register float mag = magnitude(); return Point3d(_x / mag, _y / mag, _z / mag); }
它会被转化为以下的代码:
// 以下描述内部转化 // 使用C++伪码 void normalize_7Point3dFV(register const Point3d *const this, Point3d &__result) { register float mag = this->magnitude(); // __result用以取代返回值(return value) __result.Point3d::Point3d(this->_x / mag, this->_y / mag, this->_z / mag); return ; }
这可以节省default constructor初始化锁引起的额外负担.
名称的特殊处理 (Name Mangling)
一般而言,member的名称前面会被加上 class 名称,形成独一无二的命名.例如下面的声明:
class Bar { public: int ival; };
其中ival有可能变成这样:
// member经过name-mangling之后的可能结果之一 ival_3Bar
为什么编译器要这样做?请考虑这样的派生操作(derivation):
class Foo : public Bar { public: int iva; };
记住,Foo对象内部结合了base class 和derived class 两者:
// C++伪码 // Foo的内部描述 class Foo { public: int ival_3Bar; int ival_3Foo; };
不管要处理哪一个ival,通过"name mangling",都可以绝对清楚地指出来,由于member functions可以被重载化(overloaded),所以需要更广泛的mangling手法,以提供绝对独一无二的名称,如果把:
class Point { public: void x(float newX); float x(); };
转换为:
class Point { public: void x_5Point(float newX); float x_5Point(); };
会导致两个被重载化(overloaded)的函数实体拥有相同的名称,为了让它们独一无二,唯有再加上它们的参数链表(可以从函数原型中参考得到).如果把参数类型也编码进去,就一定可以制造逐独一无二的结果,使两个x()函数有良好的转换:
class Point { public: void x_5PointFf(float newX); float x_5PointFv(); }
以上所示的只是cfront采用的编码方法,必须承认,目前的编译器并没有统一的编码方法.
把参数和函数名称编码在一起,编译器于是在不同的被编译模块之间达成了一种有限形式的类型检验,举个例子,如果有一个print函数被这样定义:
void print(const Point3d &) { ... }
但意外地被这样声明和调用:
// 以为是const Point3d & void print(const Point3d);
两个实体如果拥有独一无二的name mangling,那么任何不正确的调用操作在链接时期就因无法决议(resolved)而失败.有时候可以乐观地称此为"确保类型安全的链接行为"(type-safe linkage)."乐观地"是因为它只可以捕捉函数的标记(signature,即函数名称+参数数目+参数类型)错误;如果"返回类型"声明错误,就没有办法检查出来.
当前的编译系统中,有一种所谓的demangling工具,用来拦截名称并将其转换回去.
Virtual Member Functions (虚拟成员函数)
如果normalize()是一个 virtual member function,那么以下的调用:
ptr->normalize();
将会被内部转化为:
(*ptr->vptr[1])(ptr);
其中:
vptr表示由编译器产生的指针,指向 virtual table,它被插入在每一个"声明有(或继承自)一个或多个 virtual functions"的 class object中.事实上其名称也会被"mangled",因为在一个复杂的 class 派生体系中,可能存在有多个vptrs.
1是 virtual table slot的索引值,关联到normalize()函数
第二个ptr表示 this 指针.
类似的道理,如果magnitude()也是一个 virtual function,它在normalize()中的调用操作被转换如下:
// register float mag = magnitude(); register float mag = (*this->vptr[2])(this);
此时,由于Point3d::magnitude()是在Point3d::normalize()中被调用,而后者已经由虚拟机机制而决议(resolved)妥当,所以明确地调用"Point3d实体"会比较有效率,并因此压制由于虚拟机制而产生的不必要的重复调用操作:
// 明确的调用操作(explicity invocation)会压制虚拟机制 register float mag = Point3d::magnitude();
如果magnitude()声明为 inline 函数会更有效率,使用 class scope operator明确调用一个 virtual function,其决议(resolved)方式会和nonstatic member function一样:
register float mag = magnitude_7Point3dFv(this);
对于以下调用:
// Point3d obj; obj.normalize();
如果编译器把它转换为:
(* obj.vptr[1])(&obj);
虽然语意正确,却没有必要.请回忆那些并不支持多态(polymorphism)的对象(1.3节),所以上述经由obj调用的函数实体只可以是Point3d::normalize()."经由一个class object调用一个virtual function".这种操作应该总是被编译器像对待一般的nonstatic member function一样加以决议(resolved):
normalize_7PointdFv(&obj);
这样优化工程的另一利益是,virtual function的一个 inline 函数实体可以被扩张(expanded)开了,因而提供极大的效益利益.
版权声明:本文为博主原创文章,未经博主允许不得转载。