若是有“系统地”学习过 C++ 的模板编程,那么你应该已经知道 Expression Template 这个“东西”。在模板圣经《C++ templates》的第 18 章专门用了一整章来说这个技巧,(是的,我认为它是一种技巧)。足以见得它比较复杂,也很重要。express
提及 Expression Template 产生,“临时对象”也是“功臣”之一啊。仍是来用例子来讲明(你能很容易找到这样相似的例子,呵,我就是参照着别人写的):编程
class MyVec { public: MyVec(){ p = new int[SIZE]; } MyVec(MyVec const& a_left) { p = new int[SIZE]; memcpy(p, a_left.p, SIZE * sizeof(int)); } ~MyVec(){delete [] p;} MyVec& operator=(MyVec const& a_left) { if (this != &a_left) { delete [] p; p = new int[SIZE]; memcpy(p, a_left.p, SIZE * sizeof(int)); } return *this; } int& operator [](size_t a_idx) { return p[a_idx]; } int operator [](size_t a_idx)const { return p[a_idx]; } MyVec const operator + (MyVec const& a_left) const { MyVec temp(*this); temp += a_left; return temp; } MyVec& operator += (MyVec const& a_left) { for (size_t i = 0; i < SIZE; ++i) { p[i] += a_left.p[i]; } return *this; } private: static int const SIZE = 100; int* p; }; int main(int argc, char* argv[]) { MyVec a, b, c; MyVec d = a + b + c; return 0; }
看,咱们写下这么小段代码:dom
MyVec d = a + b + c;
这是很经常使用的数学运算吧,并且代码很直观。但这个表达式有一个问题,就是产生了“没必要要”的临时对象。由于 a + b 的结果会成为一个存在一个临时对象上 temp 上,而后这个 temp 再加上 c ,最后把结果传给 d 进行初始化。若是这些向量很长,或是表达式再加几节,能够想像这些 temp 会多让人不爽。函数
并且,若是咱们写成这样:性能
MyVec d = a;
d += b;
d += c; 学习
就能够避免产生多余的临时对象。但这样写,若是是不了解“行情”的人看了MyVec d = a + b + c;以后再看这段,是否是会以为写这代码的人欠K?优化
好吧,你会问,上面不是说右值引用能够解决这样问题?是的,但在没有右值引用的“黑暗日子”里,咱们就不用过活了?固然要,小学开始数学老师就教咱们要一题多解吧,换个思路也有办法,这个办法就是ET。this
怎么作的呢?a + b + c 会产生临时变量是由于 C++ 是即时求值的,在看到 a + b,就先算出一个 temp 的Vector对象,而后再向下算。若是能进行延迟求值,看完整个表达式再来计算,那么就能够避免这个temp的产生。lua
怎么作?spa
原来的作法中,operator + 直接进行了计算,既然咱们不想它“过早”的计算,那么咱们就在从新重载一个operator + 运算符,在这个运算中不进行真正的运算,只是生成一个对象,在这个对象中把加法运算符两边的操做数保留下来~而后让它参与到下一步的计算中去。(好吧,这个对象也是临时的,但它的代价很是很是小,咱们先不理会它)。因而咱们写下面的代码:
class MyVec; template <typename L> class ExpPlus { L const & lvec; MyVec const & rvec; public: ExpPlus(L const& a_l, MyVec const& a_r): lvec(a_l), rvec(a_r) { } int operator [] (size_t a_idx) const; }; // Point 1 template <typename L> ExpPlus<L> operator + (L const& a_l, MyVec const & a_r) { return ExpPlus<L>(a_l, a_r); } class MyVec { public: MyVec(){ p = new int[SIZE]; } MyVec(MyVec const& a_r) { p = new int[SIZE]; memcpy(p, a_r.p, SIZE * sizeof(int)); } template <typename Exp> MyVec(Exp const& a_r) { p = new int[SIZE]; for (size_t i = 0; i < SIZE; ++i) { p[i] += a_r[i]; } } ~MyVec(){delete [] p;} MyVec& operator = (MyVec const& a_r) { if (this != &a_r) { delete [] p; p = new int[SIZE]; memcpy(p, a_r.p, SIZE * sizeof(int)); } return *this; } template <typename Exp> MyVec& operator = (Exp const & a_r) { delete [] p; p = new int[SIZE]; for (size_t i = 0; i < SIZE; ++i) { p[i] += a_r[i]; } return *this; } int& operator [](size_t a_idx) { return p[a_idx]; } int operator [](size_t a_idx)const { return p[a_idx]; } private: static int const SIZE = 100; int* p; }; template <typename L> int ExpPlus<L>::operator [] (size_t a_idx) const { return lvec[a_idx] + rvec[a_idx]; } int main(int argc, char* argv[]) { MyVec a, b, c; MyVec d = a + b + c; return 0; }
比起以前的代码来讲,这段代码有几个重要的修改:首先,咱们增长了一个模板类 ExpPlus,用它来表明加法计算的“表达式”,但在进行加法时,它自己并不进行真正的计算。对这个类,定义了下标运算符,这个运算符中才进行了真正的加法计算。而后,对于原来的 MyVec,咱们重载它的赋值运算符,让它在赋值的时候经过ExpPlus的下标运算符来得到计算结果(也就是,在赋值操做时才真正的进行了计算!)。
上面这段话,对于不了解ET的人来讲,也许一时间还不容易明白,咱们一步一步来:
在 d = a + b + c 这个式子中,首先遇到 a + b,这时,模板函数 operator + 会被调用(代码中注释了“Point 1 ”),这时只是生成一个临时的ExpPlus<MyVec>对象(咱们叫它 t1 吧),不作计算,只是保留计算的左右操做数(也就是a和b),接着,t1 + c ,再次调用一样的 operator + ,并且也只是生成一个对象(咱们叫它 t2 吧),这个对象的类型是 ExpPlus<ExpPlus<MyVec>>,一样,t2 在这里只是保留了两边的操做数(也就是 t1 和 c)。直到整个表达式“作完”,没有任何东西进行了计算,所作的事情实际上只是用 ExpPlus 这个模板类把计算式的信息记录下来了(固然,这些信息就是参与计算的操做数)。
最后,当进行 d = t2 的时候,MyVec 的赋值运算符被调用(用 t2 做参数)。注意,这个调用中的语句 p[i] = t2[i],其中 t2[i] 经过重载的下标运算符,展开成 t1[i] + c[i],同理 t1[i] 又再次展开,成为 a[i]+b[i],最终,p[i] = t2[i] 就变成了:p[i] = a[i] + b[i] + c[i])(固然,里面参杂了内联的效果,这些函数都是很是容易被内联的)。就像变“魔术”同样,咱们经过ExpPlus完成了“延迟计算”,并避免了大型的 MyVec 临时对象的产生。
这基本上就是 ET 的“原理”了吧。咱们来“专门化”一下 ET 的好处:
这样,用 ET 就能兼顾到“直观”和“效率”了。
ET 中 C++ 中的类库里已经有很是多的应用了(包括 boost 中的多个子库,以及 Blitz++ 等高性能数学库)
(N)RVO 是编译器为咱们作的优化手段,在能进行优化的状况下,NRVO 的表现是很是好的,由于它才真正的避免了临时对象的产生(rvalue reference 和 expression template 中均可能还存在一些小型临时对象),但 (N)RVO 有不少的限制条件。右值引用(rvalue reference )和 move 语意弥补了 (N)RVO 的不足之处,使得临时对象的开销最小化成为可能,但这也是有局限的,好比,嗯,若是一个类自己不动态地拥有资源……,那 move 就没有意义了。Expression Template 保持了表达式直观和效率二者,很强大,但很显然它太复杂,主要是做为类库的设计者的武器。另外,它也可能使得使用者要理解一些“新”东西,好比,若是我想存储表达式的中间值,那么 <ExpPlus<ExpPlus<...<MyVec>...> 必定会让我很头大(不过有了 C++0x 的 auto 就好多了,呵呵)。
全文完!