Function Semantics

Function 语意学

Member Function的各种调用方式

Nonstatic Member Functions

C++的设计准则之一就是:成员函数要和一般函数拥有同样的效率,成员函数不应该带来额外的负担,这也就是为什么每一个成员函数的参数列表都隐含一个this指针的原因

那么member function是如何转化为nonmember function的呢?具体如下

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
28
/*1.改写函数的signature,为的是把this指针作为参数传入*/
void ClassA::foo();
//变成
void ClassA::foo(ClassA * const this);


/*2.把对nonstatic data member的存取改为经由this指针的存取*/
{
z = x * y;
//变为
this->z = this->x * this->y;
}


/*3.将member function重写为一个外部函数,将函数名进行 mangling 处理,使其独一无二*/
//例如
float Point3d::magnitude() const;
//会变成
extern magnitude_7Point3dFv(register Point3d *const this);

/*4.然后转化程序中对该member function的调用*/
obj.magnitude();
//变成
magnitude_7Point3dFv(&obj);

ptr->magnitude();
//变成
magnitude_7Point3dFv(ptr);

名称的特殊处理(Name Mangling)

其实在上面所述的步骤中,比较复杂的一点在于Name Mangling,生成独一无二的命名

之前在讨论函数重载和返回值的关系之时,也曾略微讨论过这个话题

其实到目前为止,编译器并没有统一的编码方法,不过大概的规则也就是函数名+参数个数+参数类型,因此只需知道存在此步骤即可

Static Member Functions

以下是static member functions的特性

  • 不能直接存取class中的nonstatic members
  • 不能被声明为const, volatile, virtual
  • 不需要经由class object才被调用

使用member selection语法(使用class object调用)是一种符号上的便利,实际上会转化为直接的调用操作

1
2
3
4
if (foo().object_count() > 1) {}
//变成
(void) foo();
if (Point3d::object_count() > 1) {}

如下所示,static member function的地址类型其实是nonstatic function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
class T1
{
public:
static void say()
{
std::cout << "hello" << std::endl;
}
};

int main()
{
void (*p)() = T1::say;
p();
return 0;
}

因此,可以作为回调函数,因为该函数不涉及具体的class object,可作为一般函数指针传入

Virtual Member Functions

我们都知道,对于Virtual Member Functions的调用实际上是通过vptr所指向的vtbl进行决议的

1
2
3
4
//假设foo是一个virtual member function
ptr->foo();
//会变成
(*ptr->vptr[1])(ptr);

通过vptr所指向的vtbl,再通过索引,获得对应函数指针并进行调用

但是如果是经由一个class object调用一个virtual function,这种操作总会被编译器当作一般的nonstatic member function,所进行的转化就如之前所述

为了支持virtual function 机制,首先需要对多态的对象有某种执行期类型判断法,也就是说以下的调用操作需要ptr执行期的某些相关信息

1
ptr->z();

我们需要判断这个调用是否涉及到多态,如果涉及到,应该怎么决议才能调用正确的z()

首先考虑,假设将以下信息和指针放在一起

  • 所参考的对象的地址
  • 对象类型的某种编码,用来正确决议出z()

如果是这样做的话,那么

  • 会增加空间成本
  • 不再兼容C

因此,可以考虑将额外的信息放在对象本身,因为在C++中并没有polymorphic之类的关键字,因此判断一个class是否支持多态,唯一合适的方法就是看看它是否有任何的virtual funciton,如果持有,那么它就需要这份额外的信息来支持多态机制

知道了额外信息应该放到什么地方,下一步就是决定应该存放何种额外信息

试想,如何才能正确决议一个virtual function的实例?我们应该需要什么信息才能决议?

  • ptr所指的对象的真实类型
  • z()实例的位置

为了得知以上信息,我们需要在class object中增添

  • 一个字符串/数字,标示class的类型
  • 一个指针(vptr),指向某个表格(vtbl),表格中存放virtual function的执行期地址

object中存放指向表格的指针,然后为了找到函数地址,每一个virtual function被指派一个表格索引,这些工作都是在编译期中完成的

首先我们来理一下

  • object持有的是vptr
  • class持有的是vtbl,在单继承每一个class只有一个vtbl

然后,通过下面的例子,我们很容易得出结论

  • 我们不知道p所指向的对象的类型,但是可以通过p存取到该对象的vptr,继而通过vptr获得该对象类型(class)的vtbl
    • 因为虚函数需要通过vptr进行存取,然后vptr是存放在对象中的,对象是通过构造函数进行构建的,因此,vptr的生成和赋值是在构造函数中的,这就是为什么构造函数不能是vitrual的理由
  • 我们不知道哪一个say()会被调用,但是知道每一个say()的位置都是在表中固定的位置的,对于同一个virtual function,在父类和子类的虚函数表中的索引都是一样的
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
28
29
30
31
32
#include <iostream>
class T1
{
public:
virtual void say()
{
std::cout << "T1" << std::endl;
}
};

class T2 : public T1
{
public:
void say()
{
std::cout << "T2" << std::endl;
}
};

int main()
{
T1 *p;
T1 x;
T2 y;

p = &x;
p->say();
p = &y;
p->say();

return 0;
}

Q1

接下来回答一个问题:为什么我们不知道p所指向的对象的类型呢?看都能看出来啊,既然p=&y,y是T2类型的,那么p指向的对象类型肯定是T2

就我个人理解,在程序编译时的解析中,变量的类型已经绑定好了,例如会生成如下的信息

1
2
3
<T1 *, p>;
<T1 , x>;
<T2 , y>;

p所指的内存空间,仍会解析为T1类型,因此,p不能访问到T2的方法,但是可以访问到T2和T1共有的成员变量,如vptr

我们不知道p所指向的对象的类型,其实是从编译器的角度来说的,编译器无法直接生成确定的调用,如下

1
2
3
4
5
6
7
p->say();
//无法直接生成
(T1::vtbl[n])();
//or
(T2::vtbl[n])()
//只能生成
(*p->vptr[1])(ptr);

这也就是为什么称之为动态绑定的原因

Q2

打印vtbl

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <iostream>
using namespace std;

class Base
{
public :
int base_data;
Base() { base_data = 1; }
virtual void func1() { cout << "base_func1" << endl; }
virtual void func2() { cout << "base_func2" << endl; }
virtual void func3() { cout << "base_func3" << endl; }
};

class Derive : public Base
{
public :
int derive_data;
Derive() { derive_data = 2; }
virtual void func1() { cout << "derive_func1" << endl; }
virtual void func2() { cout << "derive_func2" << endl; }
};

typedef void (*func)();


template<typename T>
void printVTBL(T &obj, int n)
{
for (int i = 0; i < n; i++)
{
unsigned long * vtbl = (unsigned long *) * (unsigned long *) &obj + i;
cout << "slot address : " << vtbl << endl;
cout << "func address : " << *vtbl << "\n" << endl;
}
}


int main()
{
Base base;
cout << "&base: " << &base << endl;
cout << "&base.base_data: " << &base.base_data << endl;
cout << "----------------------------------------" << endl;
printVTBL(base, 3);
cout << "----------------------------------------" << endl;

Derive derive;
cout << "&derive: " << &derive << endl;
cout << "&derive.base_data: " << &derive.base_data << endl;
cout << "&derive.derive_data: " << &derive.derive_data << endl;
cout << "----------------------------------------" << endl;
printVTBL(derive, 3);
cout << "----------------------------------------" << endl;

return 1;
}

多重继承下的Vitrual Functions

在多重继承的模型下,一个class可能会拥有多个vtbl

如下的继承关系

1
2
3
class Base;
class Derive;
class DD : public Base, public Derive;

所进行的构造顺序为Base->Derive->DD,所进行的析构顺序为DD->Derive->Base

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include <iostream>
using namespace std;

class Base
{
public :
int base_data;
Base() { base_data = 1; cout << "Base constructor" << endl;}
virtual void func1() { cout << "base_func1" << endl; }
virtual void func2() { cout << "base_func2" << endl; }
virtual void func3() { cout << "base_func3" << endl; }
virtual ~Base() {cout << "Base Deconstructor" << endl;}
};

class Derive
{
public :
int derive_data;
Derive() { derive_data = 2; cout << "Derive constructor" << endl;}
virtual void func1() { cout << "derive_func1" << endl; }
virtual void func2() { cout << "derive_func2" << endl; }
virtual void func3() { cout << "derive_func3" << endl; }
virtual ~Derive() {cout << "Derive Deconstructor" << endl;}
};

class DD : public Base, public Derive
{
public:
DD() {cout << "DD constructor" << endl;}
virtual void func1() {cout << "dd" << endl;};
~DD() {cout << "DD Deconstructor" << endl;}
};

typedef void (*func)();


template<typename T>
void printVTBL(T *obj, int n)
{
for (int i = 0; i < n; i++)
{
unsigned long * vtbl = (unsigned long *) * (unsigned long *) obj + i;
cout << "slot address : " << vtbl << endl;
cout << "func address : " << *vtbl << endl;
func pfunc = (func)*(vtbl);
pfunc();
std::putchar('\n');

}
}

int main()
{
DD dd;
DD *pdd = &dd;

Base *pB = &dd;
printVTBL(pB, 3);
cout << "----------------------------------------" << endl;

//Derive *pD = &dd;
//let the vptr point to Derive's VTBL
printVTBL(pB+1, 3); //pB+1(sizeof(Base)=16, 1=offset(16)) = pD

cout << "----------------------------------------" << endl;
printVTBL(&dd, 3);
cout << "----------------------------------------" << endl;

unsigned long *DDAddress = (unsigned long *)pdd;
unsigned long *BaseAddress = (unsigned long *)pB;

unsigned long *DeriveAddress = (unsigned long *)(pB+1);

cout << "DD :" << DDAddress << endl;
cout << "Base :" << BaseAddress << endl;
cout << "Derive :" << DeriveAddress << endl;

cout << "size of Base :" << sizeof(Base) << endl;
cout << "size of Derive:" << sizeof(Derive) << endl;
cout << "size of DD :" << sizeof(DD) << endl;
cout << sizeof(unsigned long) << endl;
return 1;
}

根据以上的测试代码可知,DD含有多个VTBL

pdd所在的位置存在一个vptr,指向Base所拥有的vtbl

pdd+Base得到另一个vptr,指向Derive所拥有的vtbl,因此在调用Derive的析构函数的时候会对指针进行调整

thunk技术

所谓thunk是一小段的汇编代码,用来

  • 以适当的offset调整this指针
  • 跳到virtual function中去

虚继承下的Virtual Functions

虚拟继承是多重继承中特有的概念

虚拟基类是为解决多重继承而出现的,如:类D继承自类B1、B2,而类B1、B2都继承自类A,因此在类D中两次出现类A中的变量和函数

为了节省内存空间,可以将B1、B2对A的继承定义为虚拟继承,而A就成了虚拟基类

然后,不要在virtual base class中声明nonstatic data members,因为存在共享的数据

假设在A中存在nonstatic data member,然后我调用B1的方法将其修改,就会影响到B2的访问,这就是深渊!

指向Member Function的指针

取一个不绑定与任何object的nonstatic data member的地址,得到的是在class布局中的bytes位置,如果想存取该data member,需要将其绑定到具体的class object上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#define GET_OFFSET(type, data_member) (unsigned long *)(&((type *)0)->data_member)

class Test
{
public:
int i;
char c;
int k;
};

int main()
{
std::cout << GET_OFFSET(Test, i) << std::endl;
std::cout << GET_OFFSET(Test, c) << std::endl;
std::cout << GET_OFFSET(Test, k) << std::endl;
//output
//0x0
//0x4
//0x8
return 0;
}

取一个 nonstatic member function的地址,如果该函数是nonvirtual,得到的结果是它在内存中真正的地址,但是如果不将其绑定到某个class object上,就无法通过它来调用该函数(因为nonstatic函数需要对具体的data member进行操作,因此需要绑定具体的object来进行调用,例如需要使用test.func()而不能通过Test::func()来调用func)

如果是static member function则可以看作是普通的函数,使用一般的函数指针

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
28
29
class Test
{
public:
void say()
{
}
static void foo()
{

}
};

int main()
{
Test t;

void //return value
(
Test::* //type
pmf //variable name
)
(); //argument list
pmf = &Test::say;
(t.*pmf)();

void (*p)();
p = &Test::foo;
return 0;
}

在上面的例子中,(t.*pmf)()中,t作为*this指针的提供者,只有这样才能通过对应的指针调用nonstatic member function