- 2.1 Default Constructor的构造操作
- 2.2 Copy Constructor的构造操作
- 2.3 程序转化语义学(Program Transformation Semantics)
- 2.4 成员们的初始化队伍(Member Initialization List)
- 关键词explicit之所以被导入这个语言,就是为了给程序员一个方法,使他们能够制止“单一参数的constructor”被当作conversion运算符。
2.1 Default Constructor的构造操作
“带有Default Constructor”的Member Class Object
- 如果class没有constructor,但有一个member object,而且这个object有default constructor,那么这个class的implicit default constructor是nontrivial的。举个例子,下面程序片段中,编译器为class Bar合成一个default constructor: 被合成的Bar default constructor内含代码,能够调用class Foo的default constructor处理member object Bar::foo,因为Bar::foo初始化是编译期的责任:
1
2
3
4
5
6
7class Foo { public: Foo(), Foo(int) ... };
class Bar { public: Foo foo; char *str; }
void foo_bar()
{
Bar bar; // Bar::foo 必须在此处初始化。
if (str) { } ...
}而且,被合成default constructor只满足编译期需求,并不是程序员需求。如果default constructor由程序员显式定义出来了,那么编译器的行动是:1
2
3
4
5// 为member foo调用class Foo的default construcotr
inline Bar::Bar()
{
foo.Foo::Foo();
} - 如果class A内含一个或一个以上的member class objects,那么class A的每一个constructor必须调用每一个member classes的default constructor。编译器会扩张已存在的constructors,使得user code被执行之前,先调用的default constructor。
“带有Default Constructor”的Base Class
- 如果class没有constructors,却派生自有default constructor的base class,那么这个derived class的default constructor会被视为nontrivial。
- 如果提供多个constructor,但都没有default constructor,编译器会扩张现有的每一个constructors,将用以调用必要default constructors的程序代码加进去。它不会合成一个新的default constructor。
“带有一个Virtual Function”的Class
- 另外有两种情况,也要合成default constructor:
- class声明(或继承)一个virtual function
- class派生自一个继承串链,其中有一个或更多的virtual base classes。
1
2
3graph BT
Bell --- Widget
Whistle --- Widget两个扩张在编译期间发生了:1
2
3
4
5
6
7
8
9
10
11
12
13class Widget {
public:
virtual void flip() = 0;
// ...
};
void flip(const Widget &widget) { Widget.flip(); }
void foo()
{
Bell b;
Whistle w;
flip(b);
flip(w);
} - 一个virtual function table(vtbl)被产生出来,内含class的virtual functions地址。
- 每个class object中,额外的pointer member(vptr)会被合成出来,内含相关的class vtbl地址。
1
{ *widget.vptr[1])(&widget)
“带有一个Virtual Base Class”的Class
- 看以下代码:
1
2
3
4
5graph BT
A --- X
B --- X
C --- A
C --- B因为pa的类型可以改变,编译器无法固定住foo()中经由pa而存取的X::i的实际偏移位置。编译器必须改变“执行存取操作”的那些代码,使X::i可以延迟至执行期才决定下来。cfont做法是在virtual base classes中安插指针完成。经由reference或pointer存取virtual base class的操作都可以通过指针完成。1
2
3
4
5
6
7
8
9
10
11
12class X { public: int i; } ;
class A : public virtual X { public: int j; };
class B : public virtual X { public: double d; };
class C : public A, public B { public: int k; };
// 无法在编译时期决定(resolve)pa->X::i的位置
void foo(const A *pa) { pa->i = 1024; }
main()
{
foo(new A);
foo(new C);
// ...
}__vbcX实在class object构造期间被完成的。编译器会安插允许每个virtual base class在执行器存取操作的代码,如果base没有声明任何constructor,编译器必须为它合成一个default的。1
void foo(const A *pa) {pa->__vbcX->i = 1024; }
2.2 Copy Constructor的构造操作
- 有三种情况,会以一个obect的内容作为另一个class object的初值。
- 对object做显式初始化操作
- 当object作为参数交给某个函数时
- 函数回传一个class object时
Default Memberwise Initialization
- 如果class没有提供explicit copy constructor,其内部是以default memberwise initialization手法完成的,就是把每个内建的或派生的data member(如指针或数组)的值,从object拷贝到另一个object上,不过它并不会拷贝其中的member class object。 如果String object被声明为一个class的member:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class String {
public:
// ... 没有explicit copy constructor
private:
char *str;
int len;
};
// String object的default memberwise initialization发生在这种情况下:
String noun("book");
String verb = noun;
// 完成方式好像个别设定每个members一样
verb.str = noun.str;
verb.len = noun.len;那么Word object的default memberwise initialization会拷贝_occurs,然后再于_word身上递归实施memberwise initialization。1
2
3
4
5
6
7class Word {
public:
// ... 没有explicit copy constructor
private:
int _occurs;
String _word;
};s - 一个class可用两种方式复制得到:
- 一是被初始化。以copy constructor完成。
- 二是被指定(asignment)。以copy assignment operator完成。
Bitwise Copy Semantics(位逐次拷贝)
- 一个class没有定义explicit copy constructor,是否有编译器合成实例,取决于class是否展现bitwise copy semantics而定。有两个例子:
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// 声明展现了bitwise copy semantics
// 这种情况下不需要合成default copy constructor
class Word {
public:
Word(const char*);
~Word() { delete []str; }
// ...
private:
int cnt;
char *str;
};
// 以下声明未展现出bitwise copy semantics
// 这种情况下需要合成出一个copy constructor,以便调用member class String object的copy constructor
class Word {
public:
Word(const String&);
~Word();
// ...
private:
int cnt;
String str;
};
// 其中String声明了explicit copy constructor
class String {
public:
String(const char*);
String(const String&);
~String();
// ...
};s
不要Bitwise Copy Semantics!
- 有4中情况下,class不展现出“bitwise copy semantics”:
- 当class内有个member object,而该object声明有copy constructor时;
- 当class继承自一个base class,而该class存在copy constructor时,不论是显式声明的还是被合成的;
- 当class声明了一个或多个virtual functional时。
- 当class派生自一个继承串联,其中有一个或多个virutal base clases时。
重新设定Virtual Table的指针
- 只要有一个class声明了一个或多个virtual functional,就会:
- 增加vtbl,内含virtual function的地址
- 一个指向vtbl的vptr,插在class object内。
1
2graph TD
ZooAnimal --- BearZooAnimal class object以另一个ZooAnimal class object作为初值,或Bear class object以另一个Bear class object作为初值,都可以直接靠bitwise copy semantics完成(除了member pointer)。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class ZooAnimal {
public:
ZooAnimal();
virtual ~ZooAnimal();
virtual void animate();
virtual void draw();
// ...
private:
// ...
};
class Bear : public ZooAnimal {
public:
Bear();
void animate();
void draw();
virtual void dance();
// ...
private:
// ...
}这个例子里,yogi会被default Bear constructor初始化。yogi的vptr被指向Bear的vtbl(靠安插)。1
2Bear yogi;
Bear winnie = yogi;frany的vptr不可以被指向Bear的vtbl(如果yogi的vptr被bitwise copy,会导致此结果),否则当draw()被调用而franny被传进去时,会blow up:1
ZooAnimal franny = yogi; // sliced
通过franny调用virtual function draw(),调用的时ZooAnimal而非Bear实例。事实上,yogi中的Bear部分在franny初始化时被sliced掉了,只有franny声明为reference或pointer时才会是Bear的函数实例。1
2
3
4
5
6
7
8void draw(const ZooAnimal &zoey) { zoey.draw(); }
void foo()
{
// franny的vptr指向ZooAnimal的vtbl
ZooAnimal franny = yogi;
draw(yogi); // call Bear::draw()
draw(franny); // call ZooAnimal::draw()
}
处理Virtual Base Class Subobject
- 编译器必须让derived class object中的virtual base class subobject位置在执行期准备妥当。Bitwise copy semantics可能会破坏位置的完整性,所以编译器必须必须在它自己合成出来的copy constructor中做出仲裁:
1
2
3
4graph BT
Bear -- public --- ZooAnimal
Raccoon -- public virtual --- ZooAnimal
RedPanda -- public --- Raccoon一个virtual base class的存在会使bitwise copy semantics无效。问题不在于一个class object以另一个同类的object作为初值之时,而是发生于一个class object以其derived classes的某个object作为初值之时。1
2
3
4
5
6
7
8class Raccoon : public virtual ZooAnimal {
public:
Raccoon() { /*设定private data初值*/ }
Raccoon(int val) { /*设定private data初值*/ }
// ...
private:
// ...
};
一个Raccoon object作为另一个Raccoon object的初值,bitwise copy绰绰有余,而如果企图以RedPanda object作为little_critter的初值,编译器必须判断当后续企图存取ZooAnimal subobject时是否能正确执行:在这种情况下,为了完成正确的little_critter初值设定,编译器合成一个copy constructor,安插代码设定virtual base class pointer/offset的初值(或是简单的确定它没被抹消),对每个members执行memberwise初始化操作,一起执行其他内存相关工作。1
2
3// 简单的bitwise copy还不够,必须显式将little_critter的virtual base class pointer/offset初始化
RedPanda little_red;
Raccoon little_critter = little_red;
2.3 程序转化语义学(Program Transformation Semantics)
显式的初始化操作(Explicit Initialization)
已知有定义:
1
X x0;
下面三个定义,每个都以x0来初始化class object:
1
2
3
4
5
6
7void foo_bar()
{
X x1(x0);
X x2 = x0;
X x3 = X(x0);
// ...
}以上必要的程序转化有两个阶段:
- 重写每个定义,其中初始化操作被剥除。
- class的copy constructor调用操作被安插进去。
foo_bar()可能看起来这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14C++ 伪码
void foo_bar()
{
// 定义重写,初始化操作被剥除
X x1;
X x2;
X x3;
// 编译器安插copy construction的调用操作
// 表现出X::X(const X &xx);
x1.X::X(x0);
x2.X::X(x0);
x3.X::X(x0);
// ...
}
参数的初始化(Argument Initialization)
- C++ Standard说,把class object当作参数传给函数,或者作为函数的返回值,相当于初始化操作: 这里xx是形式参数(或返回值),arg是真实参数。因此,函数:
1
X xx = arg;
会要求局部实例(local instance)x0以memberwise方式将xx当初值。代码转换为:1
2
3
4void foo(X x0);
X xx;
// ...
foo(xx);1
2
3
4
5
6// C++伪码
// 编译器产生的临时对象
X __temp0;
// 调用copy constructor
__temp0.X::X(xx);
foo(__temp0);
返回值的初始化(Return Value Initialization)
- 已知下面函数的定义: bar()的返回值如何从局部对象xx中拷贝过来?在cfont中的做法是一个双阶段转化:
1
2
3
4
5
6X bar()
{
X xx;
// ...
return xx;
}- 加上额外参数,类型是class object的reference。这个参数用来放置被拷贝构建(copy constructed)而来的返回值。
- return之前安插一个copy constructor调用。
1
2
3
4
5
6
7
8
9
10void bar(X &__result)
{
X xx;
// default constructor
xx.X::X();
// ...
// copy constructor
__result.X:XX(xx);
return; // 即使不传回任何值
}1
2
3
4X xx = bar();
// 不用default constructor,NRV
X xx;
bar(xx);1
2
3
4bar().memfunc();
// 执行bar()所传回的X class object的memfunc()可能转换
X __temp0;
(bar(__temp0), __temp0).memfunc();1
2
3
4
5
6// 同理,声明一个函数指针
X (*pf)();
pf = bar;
// 转为
void (*pf)(X&);
pf = bar;
在使用者层面做优化(Optimization at the User Level)
- 以下代码: 可以这么写:
1
2
3
4
5
6
7X bar(const T &y, const T &z)
{
X xx;
// ...以y
来处理xx
return xx;
}当bar()被转换后,效率会比较高:1
2
3
4X bar(const T &y, const T &z)
{
return X(y, z);
}1
2
3
4
5
6// C++伪码
void bar(X &__result)
{
__result.X::X(y, z);
return;
}
在编译器层面做优化(Optimization at the Compiler Level)
- 在一个像bar()这样的函数中,所有return指令传回相同具名数值(named value),因此编译器可能自己做优化: xx以_result取代:
1
2
3
4
5
6X bar()
{
X xx;
// ...
return xx;
}这样的优化操作,称为Named Return Value(NRV)。虽然NRV优化提供了重要效率改善,但它还是饱受批评。1
2
3
4
5
6
7void bar(X &__result)
{
// default constructor被调用
__result.X::X();
// ... 直接处理__result
return;
}- 优化是默默的,不透明
- 一旦函数复杂,很难优化
- 某些人不喜欢
xx0是被单一的constructor操作设定初值:1
2
3X xx0(1024); // xx0.X::X(1024);
X xx1 = X(1024);
X xx2 = (X)1024;而xx1或xx2却调用两个constructor,产生临时性object,并针对临时object调用classX的destructor:1
xx0.X::X(1024);
1
2
3
4X __temp0;
__temp0.X::X(1024);
xx1.X::X(__temp0);
__temp0.X::~X();
Copy Constructor:要还是不要?
- 一个3D坐标点类: class的default copy constructor被视为trivial。默认情况下,一个Point3d class object的“memberwise”初始化操作会导致“bitwise copy”。这样效率高,也安全。
1
2
3
4
5
6
7class Point3d {
public:
Point3d(float x, float y, float z);
// ...
private:
float _x, _y, _z;
};
实现copy constructor的最简单方法像这样:但使用C++library的memcpy()会更有效率:1
2
3
4
5
6Point3d::Point3d(const Point3d &rhs)
{
_x = rhs._x;
_y = rhs._y;
_z = rhs._z;
};1
2
3
4Point3d::Point3d(const Point3d &rhs)
{
memcpy(this, &rhs, sizeof(Point3d));
};
2.4 成员们的初始化队伍(Member Initialization List)
- 当你写下一个constructor时,就有机会设定class members的初值,不是在member initialization list,就是在constructor函数本体。
- 下列情况下,为了让你程序能够顺利执行,必须使用member initialization list:
- 当初始化reference member时;
- 当初始化const member时;
- 当调用一个base class的constructor,且拥有一组参数时;
- 当调用一个member class的constructor,而 它拥有一组参数时。
这四种情况都能正确编译并执行,但效率不高。例如:constructor可能的内部扩张结果:1
2
3
4
5
6
7
8
9class Word {
String _name;
int _cnt;
public:
Word() {
_name = 0;
_cnt = 0;
}
};一个有效率的实现方法是:1
2
3
4
5
6
7
8
9
10
11
12Word::Word( /* this pointer goes here */ )
{
// String default constructor
_name.String::String();
// tempory
String temp = String(0);
// memberwise copy _name
_name.String::operator=(temp);
// destroy tempory
temp.String::~String();
_cnt = 0;
}它会扩张:1
2
3
4Word::Word : _name(0)
{
_cnt = 0;
}1
2
3
4
5
6Word::Word( /* this pointer goes here */ )
{
// String(int) constructor
_name.String::String(0);
_cnt = 0;
}