1. 程式人生 > >13 More Effective C++—條款18/19(提前求值/臨時物件的來源)

13 More Effective C++—條款18/19(提前求值/臨時物件的來源)

1 提前求值

1 概念

上一篇介紹了“延緩求值”——lazy evalute策略,其實質是:只有在真正需要資料的時候,才對計算進行求值。同時,常用的一種的策略是“馬上求值”——eager evaluate,即只要出現計算表示式,就進行求值。

上面兩種方案都沒有考慮到,一次性大規模計算會讓使用者長時間等待。基於此,本篇提出“超急求值”——over eager evaluate。它實質是一種分攤(也可以說是分治)策略。

如下面程式碼,若otherFunc()會先於getMin()和getMax()呼叫,則m_max和m_min的計算可以放到otherFunc()中,而不必在每次需要min和max兩個值時,都對資料集進行遍歷,再求出min和max。

class ScoreList {
	public:
		void otherFunc();
		double getMin();
		double getMax();
	private:
		double m_max, m_min;
	}

這種方法通過將大規模計算分攤到其他操作中,並將計算結果永久性儲存下來,降低了單次使用者響應時間——注意,由於執行的語句數量不變,因此程式總執行響應時間不會有很大變化。

2 應用場景

下面提出如下幾種應用場景。
1,cache(快取)策略: 如果記憶體空間足夠大,可以在進行其他操作的時候,順帶將磁碟中的資料庫資料讀取到記憶體中,每次訪問某條資料,直接讀取記憶體中的變數即可。

2,prefetch(預取出)策略:cache策略只取出並暫存需要的資料。由於2/8原則,可以當前需要資料臨近區域的資料取出作進一步快取,儲存在記憶體中,從而提高資料訪問速度。

3,std::vector的空間分配:每次vector的空間耗盡時,若繼續向vector中儲存資料,系統會做如下操作:

分配新空間 -> 複製原有資料到新空間 -> 將新資料插入到新空間。

新分配的空間在記憶體擴張時,將分配2倍原有空間,以防止後續插入操作,再次引起原有資料的複製操作。

3 總結

至此,關於求值策略,我們提出三種方法:馬上求值,延緩求值,超前求值。三種方法無非就是“時間與空間的博弈”——用延長時間換空間減小,或者用空間減小換時間縮短。

2 臨時物件的來源

臨時物件並非下面程式碼所示,在函式中宣告一個變數temp,temp只是函式中的區域性物件。

void func() {
	int temp = 0;
	// do something
}

臨時變數有兩個來源:

1,隱式型別轉換。
2,函式返回的物件。

1 隱式型別轉換

如下面程式碼所示,隱式型別轉換有多種形式。

其中,情形2的轉換,只會發生在"按值傳參"和“const 引用傳參”的情況下, 這兩種允許相容的不同型別物件進行相互轉換。“引用傳參”和“指標傳參”會嚴格按照型別進行匹配,不會出現型別轉換。

// 情形1:1.23臨時轉換,得到一個臨時int型別物件,然後賦值給a
int a = static_cast<int>(1.23); 

// 情形2:實參與形參型別不一致,實參轉換型別,生成string臨時物件
int countChar(const std::string& str);
char *str = "hello world";
countChar(str); // 函式返回,臨時物件被銷燬

int countChar(std::string &str);
countChar(str); // 錯誤,char*和std::string&型別不相容

2 返回值建立臨時物件

這種情況指每次函式返回都會產生新的臨時物件。如下面程式碼所示的集中情形。

// 情形1:返回變數greet引起臨時變數建立,這個變數沒有名稱——匿名變數。
std::string createStr() {
	std::string greet;
	return greet;
}

// 情形2:呼叫operator + 操作符。a+c會產生新的臨時變數,並賦值給a
// 優化方法:使用+=操作符,a += c
MyClass a = a + c

// 情形3:呼叫“前置自增”操作符,a++
int a = 0;
a++;

針對情形1,需要特別注意不能返回函式內區域性變數的引用。如下面程式碼。

const std::string &createStr() {
	return std::string();
}

// 或者
std::string &createStr() {
	return std::string();
}

對於情形3, 需要注意前置自增實際上進行兩個操作,先獲得原始值,然後再進行自增運算。這樣會產生原始值的臨時變數。因此,若只是想做自增運算,要使用“後置自增”。

int a = 0;
++a; // 只是自增運算