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
總結
以上是生活随笔為你收集整理的Expression Template(表达式模板,ET)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Liskon替换原则
- 下一篇: 关于COCOS2D-X 中的音乐与音效应