Expression Template(表达式模板,ET)
1.前言
在前一篇文章自己实现简单的string类中提到在实现+操作符重载函数时,为了防止返回时生成的临时对象调用拷贝构造函数动态申请内存空间,使用了一个叫move的函数,它是C++0x新增的特性。既然是C++0x新增的特性,那么在以前没有这个特性的情况下,对于临时对象动态申请内存空间的问题是不是可以有其它的方法解决或避免呢?答案是肯定的,可以用Expression Template(表达式模板,ET)来解决。
2.表达式模板
对于前面的String类,我们可能经常会使用下面的表达式:
String str4 = str1 + str2 + str3;
这个表达式有一个问题,就是产生了“不必要”的临时对象。因为 str1 + str2 的结果会存放在一个临时对象 temp1上,然后temp1 + str3的结果会存放在令一个临时对象temp2上,temp2最后把结果传给str4 进行初始化。如果这些向量很长,或者表达式再加几节,不仅会产生很多的临时对象,而且要动态申请内存空间来存放String内部的字符串,这很明显是低效的。move函数解决了当+操作符重载函数返回临时对象时,编译器在栈中为其拷贝另一个对象的开销(由于+操作符重载函数内声明的临时对象temp1在函数的作用域外事无法使用的,所以编译器自动的在调用+操作符重载函数的域内声明了一个临时对象temp11,用以存放拷贝的temp1),但并没有解决产生临时对象的问题。表达式模板的思路很简单,就是对+操作进行推迟计算,即一次性计算所有的+操作,这样就不需要产生临时对象来保存+操作的临时结果。由于要推迟计算,所以必须保存str1,str2,str3操作数用于后面真正计算推迟的+操作。
3.基于String的表达式模板实现
虽然表达式模板的思路很简单,但其实现确不是想象的那么简单。原来的做法中,operator + 直接进行了计算,既然我们不想它“过早”的计算,那么我们就必须重新重载一个operator + 运算符,在这个运算中不进行真正的运算,只是生成一个对象,在这个对象中把加法运算符两边的操作数保留下来,然后让它参与到下一步的计算中去。(好吧,这个对象也是临时的,但它的代价非常非常小,因为它不需要动态申请内存空间,我们先不理会它)。
class String; template <typename L> class ExpPlus { const L &lstr; const char *rstr; const int _size; public: ExpPlus(const L & a_l, const char *a_r):lstr(a_l), rstr(a_r), _size(strlen(a_r)) { } ExpPlus(const L & a_l, const char &a_r):lstr(a_l), rstr(&a_r), _size(1) { } ExpPlus(const L & a_l, const String &a_r):lstr(a_l), rstr(a_r.c_str()), _size(a_r.size()) { } void copy_str(char *d_str) const; int size() const { return _size + lstr.size(); } }; template <typename L> void ExpPlus<L>::copy_str(char *d_str) const { lstr.copy_str(d_str); strncpy(d_str + lstr.size(), rstr, _size); } //用于+模板 template <typename L> ExpPlus<L> operator + (const L & a_l, const char & a_r) { return ExpPlus<L>(a_l, a_r); } template <typename L> ExpPlus<L> operator + (const L & a_l, const char* a_r) { return ExpPlus<L>(a_l, a_r); } template <typename L> ExpPlus<L> operator + (const L & a_l, const String & a_r) { return ExpPlus<L>(a_l, a_r); }
我们增加了一个模板类 ExpPlus,用它来代表加法计算的“表达式”,但在进行加法时,它本身并不进行真正的计算。对这个类,定义了copy_str函数,在这个函数中才进行了真正的字符串加法计算。当然,它除了支持保存String对象,还支持保存char和char *。因为String对象加字符串常量和字符也是很常见的,如下面的表达式:
String str2 = str1 + 'a' + "cd"
对于我们实现的String类,必须重新重载一个operator = 函数,用于一次性计算右边表达式的值。修改后的String代码如下:
class String { public: 。。。 template <typename Exp> String& operator=(const Exp &a_r); //新加 。。。 void copy_str(char *d_str) const; //新加 }; void String::copy_str(char *d_str) const { strcpy(d_str, _string); } template <typename Exp> String& String::operator=(const Exp &a_r) { char *temp_str; int size = a_r.size(); temp_str = new char[size + 1]; a_r.copy_str(temp_str); temp_str[size] = 0; if (_string) delete _string; _string = temp_str; _size = size; return *this; }
上面只给出了新加的代码,其它代码在自己实现简单的string类中已经给出。
上面这段话,对于不了解ET的人来说,也许一时间还不容易明白,我们一步一步来:
在 str4 = str1 + str2 + str3这个式子中,首先遇到 str1 + str2,这时,模板函数 operator + 会被调用,这时只是生成一个临时的ExpPlus<String>对象(我们叫它 t1 吧),不做计算,只是保留计算的左右操作数(也就是str1和str2),接着,t1 + str3 ,再次调用同样的 operator + ,而且也只是生成一个对象(我们叫它 t2 吧),这个对象的类型是 ExpPlus<ExpPlus<String>>,同样,t2 在这里只是保留了两边的操作数(也就是 t1 和 str3)。直到整个表达式“做完”,没有任何东西进行了计算,所做的事情实际上只是用 ExpPlus 这个模板类把计算式的信息记录下来了(当然,这些信息就是参与计算的操作数)。
最后,当进行 str4 = t2 的时候,String的赋值运算符被调用(用 t2 作参数)。注意,这个调用中的语句a_r.copy_str(temp_str),实际是是调用t2.copy_str(temp_str),t2.copy_str函数中又调用t1.copy_str,t1又调用str1.copy_str,就这样一步一步地将str1,str2,str3中的字符串拷贝到temp_str中,最终得到str4 = str1 + str2 + str3。就像变“魔术”一样,我们通过ExpPlus完成了“延迟计算”,并避免了大型的 String临时对象的产生。
4.总结
表达式模板保持了表达式直观和效率两者,很强大,但很显然它太复杂,主要是作为类库的设计者的武器。另外,它也可能使得使用者要理解一些“新”东西,比如,如果我想存储表达式的中间值,那么 <ExpPlus<ExpPlus<...<String>...> 一定会让使用者理解半天。
参考:http://www.cnblogs.com/liyiwen/archive/2009/12/03/1616627.html
http://www.cppblog.com/kesalin/archive/2009/05/28/85983.html