C++完成 vector 的四則運算。本站提示廣大學習愛好者:(C++完成 vector 的四則運算)文章只能為提供參考,不一定能成為您想要的結果。以下是C++完成 vector 的四則運算正文
這裡假定 vector 的運算界說為對操作數 vector 中雷同地位的元素停止運算,最初獲得一個新的 vector。詳細來講就是,假設 vector<int> d1{1, 2, 3}, d2{4, 5, 6};則, v1 + v2 等於 {5, 7, 9}。完成如許的運算看起來其實不是很難,一個異常直不雅的做法以下所示:
vector<int> operator+(const vector<int>& v1, const vector<int>& v2) { // 假定 v1.size() == v2.size() vector<int> r; r.reserve(v1.size()); for (auto i = 0; i < v1.size(); ++i) { r.push_back(v1[i] + v2[i]); } return r; }
// 同理,須要重載其它運算符
我們針對 vector 重載了每種運算符,如許一來,vector 的運算就與普通簡略類型無異,完成也很直白清楚明了,但明顯這個直白的做法有一個嚴重的成績:效力不高。效力不高的緣由在於全部運算進程中,每步的運算都發生了中央成果,而中央成果是個 vector,是以每次都要分派內存,假如介入運算的 vector 比擬年夜,然後運算又比擬長的話,效力會比擬低,有無更好的做法呢?
既然每次運算發生中央成果會招致效力成績,那能不克不及優化失落中央成果?回過火來看,這類 vector 的加減乘除與通俗四則運算並沒有太年夜差別,在編譯道理中,對這類表達式停止求值平日可以經由過程先把表達式轉為一棵樹,然後經由過程遍歷這棵樹來獲得最初的成果,成果的盤算是一次性完成的,其實不須要保留中央狀況,好比關於表達式:v1 + v2 * v3,我們平日可以先將其轉化為以下模樣的樹:
是以求值就釀成一次簡略的中序遍歷,那末我們的 vector 運算能否也能夠如許做呢?
表達式模板
要把中央成果去失落,症結是要推延對表達式的求值,但 c++ 不支撐 lazy evaluation,是以須要想方法把表達式的這些中央步調和狀況,用一個輕量的對象保留起來,詳細來講,就是須要可以或許將表達式的中央步調的操作數和操作類型封裝起來,以便在須要時能靜態的履行這些運算獲得成果,為此須要界說相似以下如許一個類:
enum OpType { OT_ADD, OT_SUB, OT_MUL, OT_DIV, }; class VecTmp { int type_; const vector<int>& op1_; const vector<int>& op2_; public: VecTmp(int type, const vector<int>& op1, const vector<int>& op2) : type_(type), op1_(op1), op2_(op2) {} int operator[](const int i) const { switch(type_) { case OT_ADD: return op1_[i] + op2_[i]; case OT_SUB: return op1_[i] - op2_[i]; case OT_MUL: return op1_[i] * op2_[i]; case OT_DIV: return op1_[i] / op2_[i]; default: throw "bad type"; } } };
有了這個類,我們便可以把一個簡略的運算表達式的成果封裝到一個對象外面去了,固然,我們得先將加法操作符(和其它操作符)重載一下:
VecTmp operator+(const vector<int>& op1, const vector<int>& op2) { return VecTmp(OT_ADD, op1, op2); }
如許一來,關於 v1 + v2,我們就獲得了一個異常輕量的 VecTmp 對象,而該對象可以很輕松地轉化 v1 + v2 的成果(遍歷一遍 VecTmp 中的操作數)。但下面的做法還不克不及處置 v1 + v2 * v3 如許的套嵌的龐雜表達式:v2 * v3 獲得一個 VecTmp,那 v1 + VecTmp 怎樣弄呢?
同理,我們照樣得把 v1 + VecTmp 放到一個輕量的對象裡,是以最好我們的 VecTmp 中保留的操作數也能是 VecTmp 類型的,有點遞歸的滋味。。。用模板便可以了,因而獲得以下代碼:
#include <vector> #include <iostream> using namespace std; enum OpType { OT_ADD, OT_SUB, OT_MUL, OT_DIV, }; template<class T1, class T2> class VecSum { OpType type_; const T1& op1_; const T2& op2_; public: VecSum(int type, const T1& op1, const T2& op2): type_(type), op1_(op1), op2_(op2) {} int operator[](const int i) const { switch(type_) { case OT_ADD: return op1_[i] + op2_[i]; case OT_SUB: return op1_[i] - op2_[i]; case OT_MUL: return op1_[i] * op2_[i]; case OT_DIV: return op1_[i] / op2_[i]; default: throw "bad type"; } } }; template<class T1, class T2> VecSum<T1, T2> operator+(const T1& t1, const T2& t2) { return VecSum<T1, T2>(OT_ADD, t1, t2); } template<class T1, class T2> VecSum<T1, T2> operator*(const T1& t1, const T2& t2) { return VecSum<T1, T2>(OT_MUL, t1, t2); } int main() { std::vector<int> v1{1, 2, 3}, v2{4, 5, 6}, v3{7, 8, 9}; auto r = v1 + v2 * v3; for (auto i = 0; i < r.size(); ++i) { std::cout << r[i] << " "; } }
下面的代碼英俊地處理了後面提到的效力成績,擴大性也很好並且對 vector 來講照樣非侵入性的,固然完成上乍看起來能夠不是很直不雅,除此也還有些小成績可以更完美些:
操作符重載那邊極可能會影響其余類型,是以最好限制一下,只針對 vector 和 VecTmp 停止重載,這裡可以用 SFINAE 來處置。
VecTmp
的 operator[] 函數中的 switch 可以優化失落,VecTmp 模板只需增長一個參數,然後對各類運算類型停止偏特化便可以了。
VecTmp
對保留的操作數是有請求的,只能是 vector 或許是 VecTmp<>,這裡也應當用 SFINAE 強化一上限制,使得用錯時失足信息悅目些。
如今我們來重頭再看看這一小段奇異的代碼,明顯症結在於 VecTmp 這個類,我們可以發明,它的接口其實很簡略直白,但它的類型卻可所以那末地龐雜,好比說關於 v1 + v2 * v3 這個表達式,它的成果的類型是如許的: VecTmp<vector<int>, VecTmp<vector<int>, vector<int>>>,假如表達式再龐雜些,它的類型也就更龐雜了,假如你看細心點,是否是還發明這器械和哪裡很像?像一棵樹,一棵類型的樹。
這棵樹看起來是否是還很眼生,每一個葉子結點都是 vector,而每一個外部結點則是由 VecTmp 實例化的:這是一棵類型的樹,在編譯時就肯定了。這類經由過程表達式在編譯時獲得的龐雜類型有一個學名叫: Expression template。在 c++ 中每個表達式必發生一個成果,而成果必定有類型,類型是編譯時的器械,成果倒是運轉時的。像這類運算表達式,它的終究類型是由個中每步運算所發生的成果所對應的類型組合起來所決議的,類型肯定的進程其實和表達式的辨認是分歧的。
VecTmp 對象在邏輯上其實也是一棵樹,它的成員變量 op1_, op2_ 則分離是閣下兒子結點,樹的外部結點代表一個運算,葉子結點則為操作數,一遍中序遍歷上去,獲得的就是全部表達式的值。
奇異的 boost::proto
expression template 是個好器械(就正如 expression SFINAE 一樣),它能贊助你在編譯時樹立異常龐雜好玩的類型體系(從而完成許多高等玩意,重要是函數式)。但明顯假如甚麼器械都須要本身從頭開端寫,這個技巧用起來照樣很費事苦楚的,好在模板元編程其實是個太好玩的器械,曾經有許多人做了許多前驅性的任務,看看 boost proto 吧,在 c++ 的世界裡再翻開一扇通往奇異世界的年夜門