对于下面的程序片断: ios
#include "X.h" X foo() { X xx; // ... return xx; }
可能有下面的假设:程序员
1)每次foo()被调用,就传回xx的值。算法
2)若是class X定义了一个copy constructor,那么当foo()被调用时,保证该copy数组
constructor也会被调用。安全
两个假设都视class X如何定义而定,但主要仍是视C++编译器所提供的进取优化层级函数
(degree of aggressive optimization)而定。甚至能够假设在高质量C++编译中,上述两点对性能
于class X的nontrivial definitions都不正确。优化
已知有这样的定义:this
X x0;
下面三个定义,每个都明显地以x0来初始化其class object:编码
void foo_bar() { X x1( x0 ); // 定义了x1 X x2 = x0; // 定义了x2 X x3 = X( x0 ); // 定义了x3 // ... }
必要的程序转化有两个阶段:
1)重写每个定义,其中的初始化操做会被剥除。(这里的“定义”是指“占用内存”的行
为。)
2)class的copy constructor调用操做会被安插进去。
例如上面的例子,在明确的双阶段转化以后,foo_bar()可能看起来像这样:
// 可能的转换 // C++伪码 void foo_bar() { X x1; // 定义被重写,初始化操做被剥除 X x2; // 定义被重写,初始化操做被剥除 X x3; // 定义被重写,初始化操做被剥除 // 编译器安插X copy construction的调用操做 x1.X::X( x0 ); // 表现出对copy constructor X::X( const X& xx )的调用 x2.X::X( X0 ); x3.X::X( X0 ); // ... }
把一个class object当作参数传给一个函数(或是做为一个函数的返回值),至关于如下形式
的初始化操做:
X xx = arg;
其中xx表明形式参数(或返回值)而arg表明真正的参数值。所以,若已知这个函数:
void foo( X x0 );
下面这样的调用方式:
X xx; // ... foo( xx );
将会调用局部实例(local instance)x0以memberwise的方式将xx当作初值。在编译器实
现技术上,导入所谓的临时性object,并调用copy constructor将它初始化,而后将此临时性
object交给函数。例如前一段程序转换以下:
// C++伪码 // 编译器产生出来的临时对象 X _temp0; // 编译器对copy constructor的调用 _temp0.X::X( xx ); // 从新改写函数调用操做,以便使用上述的临时对象 foo( _temp0 );
而这种方法必须改变foo()声明,形式参数必须从原先的一个class X object改变为一个class
X reference,以下:
void foo( X& x0 );
而临时性的object也会被class X的destructor析构掉。
另外一种实现方式是以“拷贝建构”(copy construct)的方式把实际参数直接建构在其应该的位
置上,此位置视函数活动范围的不一样,记录于程序堆栈中。并在程序返回以前用destructor析
构。
已知下面这个函数定义:
X bar() { X xx; // 处理 xx... return xx; }
关于bar()的返回值如何从局部对象xx中拷贝过来,Stroustrup在cfront中的解决作法是一个
双阶转化:
1)首先加上一个额外参数,类型是class object的一个reference。这个参数将用来放置被
“拷贝建构(copy constructed)”而得的返回值。
2)在return指令以前安插一个copy constructor调用操做,以便将欲传回之object的内容当
作上述新增参数的初值。
bar()按上述算法转换以下:
// 函数转换 // 以反映出copy constructor的应用 // C++伪码 void bar( X& _result ) // 加上一个额外的参数 { X xx; // 编译器所产生的default constructor调用操做 xx.X::X(); // 编译器所产生的copy constructor调用操做 _result.X::X( xx ); return; }
如今编译器必须转换每个bar()调用操做,以反映其新定义。以下:
X xx = bar();
将被转换为下列两个指令句:
// 注意,没必要施行default constructor X xx; bar( xx );
而:
bar().memfunc(); // 执行bar()所传回之X class object的memfunc()
可能被转化为:
// 编译器所产生的临时性对象 X _temp0; ( bar( _temp0 ), _temp0 ).memfunc();
同理,若是程序声明了一个函数指针,像这样:
X ( *pf )(); pf = bar;
它也必须被转化为:
void ( *pf )( X& ); pf = bar;
“程序员优化“:定义一个”计算用“的constructor。即:
// 程序员再也不写下面的函数 X bar( const T &y, const T &z ) { X xx; // ...以y和z来处理xx return xx; // 这会要求xx被”memberwise“地拷贝到编译器所产生的_result之中 } // 能够定义另外一个constructor,直接结算xx的值 X bar( const T &y, const T &z ) { return X( y, z ); } // 这样转换后能够效率比较高 // C++伪码 void bar( X &_result, const T &y, const T &z ) { _result.X::X( y, z ); return; }
能够看到_result直接被计算出来,而不是经由copy constructor拷贝而得。这种作法可能致使
特殊计算用途的constructor大量扩散。在这个层面上,class的设计是以效率考虑居多,而不是
以”支持抽象化“为优先。
在像bar()这样的函数中,全部的return指令传回相同的具名数值,所以编译器可能本身作优
化,方法是以result参数取代named return value。例以下面bar()定义:
X bar() { X xx; // ...处理xx return xx; }
编译器把其中的xx以_result取代:
void bar( X &_result ) { // default constructor被调用 // C++伪码 _result.x::X(); // ...直接处理_result return; }
这样的编译器优化操做,有时候被称为Named Return Value(NRV)优化。考虑以下代
码:
class test { friend test foo( double ); public: test(){ memset( array, 0, 100 * sizeof( double ) ); } private: double array[ 100 ]; }; // 下面函数产生、修改并传回一个test class object test foo( double val ) { test local; local.array[ 0 ] = val; local.array[ 99 ] = val; return local; } // main函数调用上述foo()函数1000万次 int main() { for( int cnt = 0; cnt < 10000000; cnt++ ) { test t = foo( double( cnt ) ); } return 0; } // 整个程序的意义是:重复循环10000000次,每次产生一个test object; // 每隔test object配置一个拥有100个double的数组:全部的元素都设初值 // 为0,只有#0和#99元素以循环计数器的值做为初值
上面这个版本不能实施NRV优化,由于test class缺乏一个copy constructor。第二版加上一
个inline copy constructor以下:
inline test::test( const test &t ) { memcpy( this, &t, sizeof( test ) ); }
获得的程序以下:
#include <iostream> #include <memory.h> class test { friend test foo( double ); public: test(){ memset( array, 0, 100 * sizeof( double ) ); } inline test( const test &t ){ memcpy( this, &t, sizeof( test ) ); } private: double array[ 100 ]; }; // 下面函数产生、修改并传回一个test class object test foo( double val ) { test local; local.array[ 0 ] = val; local.array[ 99 ] = val; return local; } // main函数调用上述foo()函数1000万次 int main() { for( int cnt = 0; cnt < 10000000; cnt++ ) { test t = foo( double( cnt ) ); } return 0; }
下面是各类版本的运行性能分析(经过gprof和time程序)结果:
1)未实施NRV(源程序为copyCtr4.cc)
2)实施NRV(源程序为copyCtr5.cc)
3)实施NRV+-O1(源程序为copyCtr5.cc)
一样也能够从编译器生成的汇编代码看到:
// 未实施NRV优化的main函数 main: .LFB975: ... jmp .L5 .L6: ... call _Z3food subl $4, %esp addl $1, -812(%ebp) .L5: cmpl $9999999, -812(%ebp) jle .L6 movl $0, %eax movl -4(%ebp), %ecx ... // 实施NRV优化的main函数 main: .LFB978: ... jmp .L5 .L6: ... call _Z3food subl $4, %esp addl $1, -812(%ebp) .L5: cmpl $9999999, -812(%ebp) jle .L6 movl $0, %eax movl -4(%ebp), %ecx ... // 实施NRV优化和-O1优化的main函数 main: .LFB1031: .loc 1 29 0 .cfi_startproc .LVL3: subl $800, %esp .cfi_def_cfa_offset 804 .loc 1 29 0 movl $10000000, %eax .LVL4: .p2align 4,,7 .p2align 3 .L24: .LBB37: .loc 1 30 0 discriminator 2 subl $1, %eax jne .L24 .LBE37: .loc 1 36 0 xorl %eax, %eax addl $800, %esp .cfi_def_cfa_offset 4 .LVL5: ret
结果是明显的,NRV并无太多明显的改善(经过main函数对比看出,可能gcc自动实施了
NRV优化,致使二者main函数相同),并且随着编译器技术的进步,在-O1优化面
前,单独的NRV优化更多成了后辈程序员们对历史的一种记念(只是从结果看到的效率出发,
并无验证-O1优化后程序的正确性)。
已知下面的3D坐标点类:
class Point3d { public: Point3d( float x, float y, float z ); // ... private: float _x, _y, _z; };
上述class的default copy constructor被视为trivial。它既没有任何member(或
base)class objects带有copy constructor,也没任何的virtual base class或virtual function。所
以,默认状况下,一个Point3d class object的“memberwise”初始化操做会致使“bitwise copy”。
这样作效率很高,并且很安全,由于三个坐标成员是以数值来存储的。bitwise copy既不
会致使memory leak,也不会产生address aliasing。
对于这个class,不必提供一个explicit copy constructor,由于编译器自动实施了最好的
行为。而若是预见class须要大量的memberwise初始化操做,则提供一个copy constructor的
explicit inline函数实例就是很是合理的了——在“编译器提供NRV优化”的前提下。
实现copy constructor的最简单方法像这样:
Point3d::Point3d( const Point3d &rhs ) { _x = rhs._x; _y = rhs._y; _z = rhs._z; };
而C++ library的memcpy会更有效率:
Point3d::Point3d( const Point3d &rhs ) { memcpy( this, &rhs, sizeof( Point3d ) ); }
然而不论是memcpy()仍是memset(),都只有在“classes不含任何由编译器产生的内部
members”时才能有效运行。若是Point3d class声明一个或一个以上的virtual functions,内含一
个virtual base class,那么使用上述函数将会致使那些“被编译器产生的内部members”的初值被
改写。例如:
class Shape { public: // 这会改变内部的vptr Shape(){ memset( this, 0, sizeof( Shape ) ); } virtual ~Shape(); // ... }
编译器为此constructor扩张的内容看起来像这样:
// 扩张后的constructor // C++伪码 Shape::Shape() { // vptr必须在使用者的代码执行以前先设定稳当 _vptr_Shape = _vtbl_Shape; // memset会将vptr清为0 memset( this, 0, sizeof( Shape ) ); };
要正确使用memset()和memcpy(),则须要掌握某些C++ Object Model的语意学知识!
4、成员们的初始化队伍(Member Initialization List)
在下列状况下,为了让程序顺利经过编译,必须使用member initialization list:
1)当初始化一个reference member时;
2)当初始化一个const member时;
3)当调用一个base class的constructor,而它拥有一组参数时;
4)当调用一个member class的constructor,而它拥有一组参数时;
在这种状况下,程序能够被正确编译并执行,可是效率不高。例如:
class Word { public: Word(){ _name = 0; _cnt = 0; } private: String _name; int _cnt; }
在这里,Word constructor会先产生一个临时性的String object,而后将它初始化,以后以
一个assignment运算符将临时object指定给_name,随后再摧毁那个临时性object。可能扩张结
果以下:
// C++伪码 Word::Word( /*this pointer goes here*/ ) { // 调用String的default constructor _name.String::String(); // 产生临时性对象 String temp = String( 0 ); // "memberwise"地拷贝_name _name.String::operator=( temp ); // 摧毁临时性对象 temp.String::~String(); _cnt = 0; }
下面是一个明显更有效率的实现方法:
// 较佳的方式 Word::Word : _name( 0 ) { _cnt = 0; }
它会被扩张成这样子:
// C++伪码 Word::Word( /*this pointer goes here*/ ) { // 调用String( int )constructor _name.String( 0 ); _cnt = 0; }
陷阱最有可能发生在这种形式的template code中:
template< class type > foo< type >::foo( type t ) { // 多是(也可能不是)个好主意 // 视type的真正类型而定 _t = t; }
这会致使member初始化权在member initialization list中完成,甚至一个行为良好的
member,如_cnt:
// 坚持此种编码风格 Word::Word() : _cnt( 0 ), _name( 0 ) { }
那么member initialization list中到底发生了什么?
编译器会一一操做initialization list,以适当顺序在constructor以内安插初始化操做,而且
在任何explicit user code以前。例如,先前的Word constructor被扩充为:
// C++伪码 Word::Word( /*this pointer goes here*/ ) { _name.String::String( 0 ); _cnt = 0; }
实际上:list中的项目顺序是由class中的members声明顺序决定的,不是由initialization
list中的排序顺序决定的。本例的Word class 中,_name被声明于_cnt以前,因此它的初始化也
比较早。
“初始化顺序”和“initialization list中项目的排列顺序”之间的外观错乱,会致使意想不到的
危险:
#include <stdio.h> class X { public: int i; int j; public: X( int val ) : j( val ), i( j ){ } // 有陷阱的写法 }; class Y { public: int i; int j; public: Y( int val ) : j( val ){ i = j; } // 修改后的写法(建议写法) }; int main() { X x( 3 ); Y y( 5 ); printf( "x.i = %d x.j = %d\n", x.i, x.j ); printf( "y.i = %d y.j = %d\n", y.i, y.j ); return 0; }
能够看到x.i果真不是所指望的3。initialization list的项目被放在explicit code以前。
能够像下面这样,调用一个member function以设定一个member的初值:
// X::xfoo()被调用 X::X( int val ) : i( xfoo( val ) ), j( val ) {}
其中xfoo()是X的一个member function。但咱们不知道foo()对X object的依赖性有多高,如
果把xfoo()放在constructor体内,那么对于“到底哪个member在xfoo()执行时间被设立初值”这
件事,就能够确保不会发生模棱两可的状况了。
Member function的使用是合法的,这是由于和此object相关的this指针已经被建构稳当,
而当constructor大约被扩充为:
// C++伪码:constructor被扩充后的结果 X::X( /*this pointer, */ int val ) { i = this->xfoo( val ); j = val; }
最后,若是一个derived class member function被调用,其返回值被当作base class
constructor的一个参数,将会如何:
// 调用FooBar::fval() class FooBar : public X { public: int fval(){ return _fval; } FooBar( int val ) : _fval( val ), X( fval() ){ } // fval()做为base class constructor的参数 private: int _fval; }
它的可能扩张结果:
// C++伪码 FooBar::FooBar( /* this pointer goes here */ ) { X::X( this, this->fval() ); //的确不是一个好主意 _fval = val; };