C++中编译器背着程序员做了很多事情,导致程序员无法准确的了解到自己所实现的代码本身所具有的性能缺陷。这一章主要谈论C++中构造函数相关的构建行为。
1 默认构造函数的构建行为
一般来说,我们认为在构建一个类时,如果程序员并未给类提供任何相关的构造函数,编译器总是会自动生成默认构造函数,但是实际上的行为却并不完全一致,并且编译器一般不会负责初始化类中的成员对象。但是,在一些情况下,编译器的确会生成相关的默认构造函数来完成一些元素的初始化任务。而具体何时生成一般主要看编译器本身是否需要。言外之意,就是如果编译器不需要生成,并且用户也没有声明default constructor时,生成的default constructor本身就是一个没有意义的函数。一般下列四种情况下编译器会生成一个default constructor:
- 带有default constructor的类成员:当当前类拥有一个成员变量,而该变量是一个拥有显式声明的default constructor的类时,编译器为了初始化该类会自动声明一个default constructor,并在自动生成的default constructor中调用该类的构造函数。
- 如果当前类并未声明任何构造函数,则编译器声明一个default constructor,并在该构造函数中初始化成员类;
- 如果当前类包含多个已经声明的构造函数,则编译器会扩张已经存在的每一个构造函数,并在用户代码之前调用类成员的default constructor;
- 如果当前类中包含多个类成员变量,则编译器会按照各个类成员声明的顺序在自动生成的构造函数中调用类成员的default constructor或者扩张已经存在的constructor;
- 带有default constructor的基类:如果当前类继承体系中,其祖先类中存在一个包含default constructor的类,则为了初始化该类编译器会生成default constructor并插入相关初始化代码,如果存在则选择扩张constructor;
- 带有virtual function的类:当前类包含一个虚函数或者继承自包含虚函数的类,即类最终包含一个虚函数表指针。编译器生成的default constuctor或者扩张constructor的目的是初始化虚函数表和虚函数指针,基本行为和上面的类似;
- 当前类虚继承自另一个类:虚继承涉及到了比较复杂的机制,他会在构造函数参数列表中生成一个bool类型变量,只有在顶层子类中,才会通过bool类型量实例化一次虚基类中的对象,以此实现虚基类机制。
从上面的内容能够看到的是,编译器需要的意思是,如果类中存在需要编译器负责初始化的对象时,编译器会声明不存在的default constructor或者扩张已经存在的constructor,而如果需要程序员自己初始化的对象,则并不会生成有用的default constructor。
2 拷贝构造的构建行为
一般程序中涉及到拷贝构造函数调用的场景有:直接赋值,对象作为返回值,对象作为函数参数。和default constructor类似,拷贝构造函数面临同样的问题:当用户并未声明具体的拷贝构造函数时,编译器会按照自己的需要生成或扩展拷贝构造函数。
- memberwise initialization:即对象会根据对象的类型进行相应的初始化,比如如果是基本类型则直接拷贝,如果是类对象并且该类对象不是bitwise copy semantics,则会调用类对象的拷贝构造函数对类进行memberwies initialization;
- bitwise copy semantics:顾名思义,就是类能够在能够在内存中按位存取进行初始化和拷贝。
形式化的说明,以下四种情况不会展现bitwise copy semantics:
- 当类中包含一个类成员对象,而该对象声明了以讹copy constructor;
- 当类继承自一个声明了copy constructor的类;
- 当类声明了至少一个虚函数;
- 当类的继承链中,其上层类中存在虚继承。
以上四种情况中拷贝构造函数的行为和上面讲到的default constructor的行为类似,要么声明不存在的copy constructor,对对象进行初始化,要么扩展已经存在的copy constructor,在用户代码之前对类对象进行拷贝。
3 程序转化语义学
NRV优化:即如果程序的函数将一个对象作为返回值返回并将该返回值赋值给另一个作用域之外的对象时,编译器可能会修改函数,将函数外需要赋值的对象的引用作为参数调用进函数中,直接在函数中拷贝该返回值,这样可以少几次拷贝构造函数的调用。
X func()
{
X xx;
return x
}
X ret = func();
NRV优化会导致上面代码可能以如下方式进行:
void func(X &_ret)
{
_ret.X::X();
return;
}
func(ret);
但是,书中提到只有用户显示声明拷贝构造函数时,NRV优化才可能被激活。部分编译器可以通过显示的拷贝构造函数的声明来显示使用NRV优化,部分编译器会默认调用这个优化。但是NRV优化也不全是优点:
- 优化由编译器默认完成,是否真的进行了NRV,程序员并不清楚;
- 一旦函数变得复杂,优化也就难以进行;
- 程序员可能不喜欢这种优化,这种优化可能打乱程序员本身对代码的优化;
因此,在对类设计时,是否真的需要声明拷贝构造函数需要深思熟虑,声明可能会导致NRV优化,无法实现bitwise copy semantics,效率不一定有提升,不声明可能丢掉NRV本身可能带来的性能优势。
4 参数化列表初始化
在构造函数中,一定要使用参数化列表进行初始化的情况有:
- 当初始化一个引用成员;
- 当初始化一个const成员;
- 当调用父类的constructor,而它有一组参数时;
- 当调用类成员变量的constructor,而它有一组参数时。
需要注意的是,参数化列表初始化并不是函数调用,它是由编译器维护的一系列行为。并且参数化列表的初始化顺序是按照类中成员变量的声明顺序进行的,而不是参数化列表的顺序。在初始化列表中还可以调用类对象本身的成员函数对成员进行初始化,因为此时this指针已经安插到了参数列表中。但是不可以使用类成员对另一个类成员进行初始化,因为此时类成员可能未构造完成。来自编译器的保证,成员初值列一定会在构造函数体运行之前运行,即初始化列表的操作可以认为会插入在构造函数的用户代码之前。