问题引入
考虑下面的代码:
#include "X.h"
X foo()
{
X xx;
// ...
return xx;
}
《对象模型》为此给出了两个命题:
- 每次
foo()
被调用,就返回xx
的值。 如果
X
定义了一个copy constructor,当foo()
被调用时,该constructor也会被调用。
命题1的真假与X
的定义有关,命题二的真假与X
有关,但更取决于编译器的进取性优化层级(degree of aggressive optimization)。显示初始化操作
已知:
X x0;
有三种方式,可以使用
x0
初始化X
对象:void foo_bar() { X x1(x0); X x2 = x0; X x3 = X(x0); }
foo_bar()
可能被转化成:void foo_bar() { X x1; X x2; X x3; x1.X::X(x0); x2.X::X(x0); x3.X::X(x0); }
参数初始化
C++标准说,把一个对象当作参数传给一个函数,相当于进行了这样的初始化:
X xx = arg;
。
其中xx
代表形参,arg
代表真正的参数。如果foo()
接受一个X
类型的参数:void foo(X x0);
,并且进行如下的调用:X xx; foo(xx);
局部实例
x0
会把xx
作为初值,以member-wise的方式进行初始化。具体到编译器的具体实现,有一种手法是使用临时的对象,通过copy constructor将它初始化,把它的引用作为参数传给函数。这使得上面的代码被转化为:X _tempX; _tempX.X::X(xx); foo(_tempX); void foo(X& x0); _tempX.X::~X();
返回值的初始化
已知函数定义:
X bar() { X xx; // ... return xx; }
bar()
的返回值如何从局部对象xx
中拷贝?cfront中的解决方式是双阶段转化:void bar(X& _result) { X xx; xx.X::X(); // ... _result.X::X(xx); return; }
这使得
X xx = bar();
看起来像是:X xx; bar(xx);
如果
X
有成员函数X::memfunc()
,则bar().memfunc()
可能被转化为:X _tempX; (bar(_tempX), _tempX).memfunc(); //书中这样写,有些疑惑
同样的,如果有一个函数指针指向
bar()
:X (*pf)(); pf = bar;
会被转化为:
void (*pf)(X&); pf = bar;
在使用者层面做优化
考虑下面的代码:
X bar(const T& y, const T& z) { X xx; // 使用y和z处理xx return xx; }
对于
bar()
,Jonathan Shopiro提出程序员优化的概念,为X
定义一个constructor,以y
和z
作为参数:X bar(const T& y, const T& z) { return X(y, z); }
前文已经讲过,
bar()
可能被转化为:void bar(X& _result) { _result.X::X(y, z); return; }
由于省去了copy constructor的调用,这种方法效率更高,代价是需要额外定义一个constructor。
在编译器层面做优化
前面讲过了对
bar()
可能的转化,编译器可以使用该方法做优化,甚至可以省去对copy constructor的调用:X bar() { X xx; // ... 处理xx return xx; }
被优化为:
void bar(X& _result) { _result.X::X(); // ... 处理——result return; }
这样的优化被称为NRV(Named Return Value)优化,并在C++标准中广泛使用。下面的代码是NRV优化的一个测试用例:
#include <iostream> #include <chrono> class test { friend test foo(double); public: test() { memset(array_d, 0, 100 * sizeof(double)); } test(const test& t) { memcpy(this, &t, sizeof(test)); } private: double array_d[100]; }; test foo(double val) { test local; local.array_d[0] = val; local.array_d[99] = val; return local; } int main() { std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now(); for (int cnt = 0; cnt < 10000000; cnt++) { test t = foo(double(cnt)); } std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now(); std::cout << std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() << "us"; return 1; }
只有当
test
定义了copy constructor时才会施行NRV优化。我的观点是,这句话需要结合bar()
潜在的语义进行理解,bar()
只是构造了一个X
对象的实例并在处理后返回,它的实现中并没有要求调用copy constructor的语义,因此没有必要再在bar()
中引入一个临时的X
,通过copy constructor将它复制给真正的返回参数。所以实施了优化后,用户定义的copy constructor不会被调用。我在MSVC上进行测试,releas下copy constructor里的打印操作没有执行,而在debug下是会执行的。此外,MSVC中加入了copy constructor,并没有获得多少性能提升2333,这个现象和书中是一致的。是否需要copy constructor
定义一个三维坐标点类:
class Point3d { public: Point3d(float x, float y, float z); private: float m_x, m_y, m_z; };
需要为其显示定义copy constructor吗?显然它的default copy constructor是trivial的,因此member-wise初始化对它来说施行的是bit-wise copy,效率高且安全,你没有理由再定义一个copy constructor。但如果情况是
Point3d
需要大量的member-wise初始化,比如前面的以传值被返回,就有必要实现一个copy constructor激活NRV。此时,你可能觉得直接在copy constructor中使用memcpy
是个不错的主意,但请注意,这只在类中不含有任何由编译器产生的成员时才成立。否则,类似vptr之类的成员会被改写。
0 条评论