原创作品,转载请保留版权信息。
上一篇文章我讨论了一个表达式模板的例子。因为我也是在学习C++的过程中,上一篇文章并没有能把这个类的设计思想吃透,而且也有一些技术细节没有讲清楚。这一片文章就是为了进一步弄清楚这个表达式模板。只有知其然而且能够知其所以然,我们才能够自己设计出类似功能的类,才能够有本质的提高。希望专家多提意见。
首先,我们在上一个程序的基础上加入VecSub, VecMul, VecDiv,以及operator-, operator*, operator/. 这样我们可以支持比较复杂的运算,比如:
c=(a+b)*(a-b);
上一篇文章没有讲述的一些技术细节:
template:模板,如果理解为一个类型参数,实际上这个还比较好理解的。请参看有关书籍。
另外一个重要的概念:
我们看到,我们定义的operator+, operator-, operator*, operator/其形参类型均为基类引用,即VecExpression<E>, E代表一种具体的表达式类型,他们可以是Vec, VecAdd, VecSub, VecMul, VecDiv。一个问题是,基类可以正确的表达我们这些具体类型吗?直接了当的写法是为每一个类型写一个operator+/operator-/operator*/operator/ 这样的重载函数. 这样就会导致很多这样的函数,每加一类新的操作,我们就必须加一类这样的操作。
这个程序里提供了一个解决这个问题的方法:使用基类指针或者引用。其原理是:
在引用或指针时,派生类的对象可以当作基类的对象看待和处理。即可以用Derived* 或Derived& 对base* 或base&赋值。反过来基类到派生类则需要显式转换。这就是为什么operator+的参数均为基类引用,而可以传进去不同的派生类引用。
反过来,base* 或base& 要对derived* 或Derived&赋值时,需要显式转换,我们看到,基类里提供的两个转换函数就是为了把基类引用强制转换为派生类引用而引入的。
我们如何理解这个简单的表达式:
Vec a, b;
a+b
第一步:调用operator+(const VecExpression<Vec>&,const VecExpression<Vec>&). 实参类型为:(const Vec&, const vec&), 基于上面的原理,这个调用时不需要转换的。
第二步:调用VecAdd构造函数,VecAdd<E1,E2>(u,v), 此时,E1,E2被替代为Vec, 因为形参类型为VecExpression<Vec>. 我们看到VecAdd里的两个引用成员此时类型为:Vec&, 但是构造函数传入的实参类型此时已经是基类类型VecExpression<Vec>, 在将一个基类引用赋值给一个派生类引用时,需要类型转换,这时候基类类型转换成员函数就被调用:Vec const &, 也就是说:_u(u)这句话被转换为:_u=(Vec const&)u (_u类型为Vec&, u类型为VecExpression<Vec>).
我们再看复合表达式:
c=(a+b)*(a-b)
1. 执行(a+b),得到对象VecAdd(Vec&,Vec&)
2.执行(a-b),得到对象VecSub(Vec&,Vec&)
3.调用operator*,得到对象VecMul(VecAdd(Vec&,Vec&),VecSub(Vec&,Vec&))
4. 等号时执行对应元素操作。
事实上这个简单的类已经可以实现Vec之间的任意复合运算了。如果加入标量与向量之间的运算(这个也是相当直接的,读者可以自己试试),这个向量表达式模板类已经相当功能齐全了。如果加入矩阵与向量的运算,那么这个类可以解决很多类型的问题了。
到了此时,我们可以看到表达式模板的几个主要思想:
1。一个抽象的表达式类作为基类。所有的其它操作,操作数,均是这个抽象类的派生。
2。所有的操作均基于基类,由基类的转换函数得到正确的操作。
由此我们看出,设计出功能强大,令人印象深刻的程序,面向对象的思想和把问题合理抽象是极其重要的。