最佳化 C++/程式碼最佳化/構造和析構
通常情況下,在處理表達式時,會建立一個臨時物件,該物件在表示式結束時被銷燬。如果該物件是基本型別,編譯器幾乎總是能夠避免其建立,而且基本型別物件的建立和銷燬速度相當快。相反,如果該物件是複合型別,其建立和銷燬成本無限,因為它們會導致建構函式和解構函式的呼叫,這些函式可能需要任何時間。
在本節中,將介紹一些避免建立複合臨時物件的技術,從而避免呼叫其建構函式和解構函式。
對於非行內函數,嘗試宣告一個返回值型別,其物件複製移動不超過 8 位元組。如果不可行,至少在 `return` 語句中構造結果物件。
在編譯非行內函數時,編譯器無法知道返回值是否會被使用,因此必須生成它。生成和分配一個複製移動不超過 8 位元組的物件幾乎沒有成本,但生成和分配更復雜的物件則需要時間。如果臨時物件擁有資源,所花費的時間會更大,但即使沒有分配,所花費的時間也會隨著該物件所使用的機器字的數量而增加。
然而,如果要返回的物件是在 `return` 指令本身中構造的,因此沒有將該值分配給變數,那麼語言標準保證了一種稱為 _返回值最佳化_ 的最佳化,它可以阻止建立臨時物件。
一些編譯器可以成功地避免建立臨時物件,即使返回的物件與區域性變數相關聯(使用所謂的 _命名返回值最佳化_),但這通常沒有保證,並且有一些限制。 C++ 常見問題解答
要檢查是否應用了上述最佳化之一,請在返回物件類的每個建構函式、解構函式和賦值運算子中遞增一個靜態容器。如果沒有應用任何最佳化,請使用以下替代技術之一
- 將 `void` 設為函式返回型別,並向其新增一個按引用傳遞的引數,充當返回值。
- 將函式轉換為返回型別的建構函式,接受相同的函式引數。
- 使函式返回一個輔助型別的物件,該物件從返回值物件中竊取資源,並將它們傳遞給目標物件,而無需複製其內容。
- 使用一個 _表示式模板_,這是一種高階技術,是稱為 _模板超程式設計_ 的程式設計正規化的組成部分。
- 如果使用 C++11 標準,請使用 _右值引用_。
如果在迴圈體中聲明瞭一個變數,並且對其進行賦值的成本小於構造和析構的成本,請將該宣告移到迴圈之前。
如果在迴圈體中聲明瞭該變數,則相關聯的物件會在每次迭代時構造和析構,而如果它在迴圈之外,則該物件只構造和析構一次,但在迴圈體中可能會多賦值一次。
不過,在很多情況下,賦值的成本與構造和析構對的成本完全相同,因此在這種情況下,將宣告移出迴圈並新增迴圈內的賦值不會有任何收益。
在賦值運算子過載(operator=)中,如果您確定它永遠不會丟擲異常,請複製每個成員變數,而不是使用 _複製和交換_ 慣用法。
複製物件的最高效方法是模仿複製建構函式的適當初始化列表,也就是說,首先呼叫基類的類似成員函式,然後按宣告順序複製每個成員變數。
不幸的是,這種技術不是 _異常安全的_,也就是說,如果在此操作期間丟擲了異常,一些已構造的子物件的解構函式可能永遠不會被呼叫。因此,如果在複製過程中有可能會丟擲異常,您必須使用一種 _異常安全_ 的技術,儘管它不會具有最佳效能。
最優雅的 _異常安全_ 賦值技術是稱為 _複製和交換_ 的技術。它由以下程式碼示例,其中 `C` 表示類的名稱,而 `C` 是要定義的成員函式
C& C::operator=(C new_value) {
swap(new_value);
return *this;
}
為了避免代價高昂的轉換,請為最常見的引數型別定義過載函式。
假設您編寫了以下函式
int f(const std::string& s) { return s[0]; }
其目的是允許編寫以下程式碼
std::string s("abc");
int n = f(s);
但它也可以被以下程式碼使用
int n = f(string("abc"));
並且,由於從 `char*` 到 `std::string` 的隱式轉換,它也可以被以下程式碼使用
int n = f("abc");
最後兩個對 `f` 函式的呼叫效率都很低,因為它們建立了一個臨時的非空 `std::string` 物件。
為了保持第一個示例呼叫的效率,您必須定義以下函式過載
int f(const char* s) { return s[0]; }
一般來說,如果一個函式在呼叫時傳遞了一個意外型別但可以隱式轉換為預期型別的引數,則會建立一個預期型別的臨時物件。
為了避免這種臨時物件,您必須定義一個原始函式的過載,它接受實際傳遞的物件型別的引數,從而避免了轉換的需要。