本文討論了何時C++會自動進行移動操作,并且說明了復制消除,RVO和NRVO優化,
移動操作主要參考了cppreference 的這個說明,
優化部分的主要的參考來自于stack overflow 的這篇文章,
移動操作
移動操作有關的函式
和移動操作相關的類函式有兩個:
移動建構式:
A(A&& rhs);
移動賦值運算子:
A& operator=(A&& rhs);
注意這兩個函式的引數型別都不是const,這也是C++默認會生成的函式宣告,
移動建構式用于在構造型別的時候使用:
A a1;
// 使用std::move強制進行移動
A a2 = std::move(a1);
或
A a2(std::move(a1));
而移動賦值運算子就是在賦值的時候進行移動:
A a1;
A a2;
a1 = std::move(a2); // 使用move進行強制移動
何時自動宣告移動建構式和賦值移動建構式
隱式的移動建構式將會在可以被生成且滿足如下所有條件的情況下自動生成:
- 沒有用戶宣告的 復制建構式
- 沒有用戶宣告的 復制賦值運算子(即
operator=(const A&)這類) - 沒有用戶宣告的 移動賦值運算子(即
operator=(A&&)這類) - 沒有用戶宣告的 解構式
所謂可以被生成的意思是滿足以下所有條件:
- 類中沒有不能移動的非靜態成員
- 繼承時,基類可以被移動
- 繼承時,基類的建構式可以被訪問
而移動賦值運算子的產生條件也差不多,只不過將沒有宣告的 移動賦值建構式改成沒有用戶宣告 移動建構式即可,
總之,這兩個函式生成的條件就一句話:除了普通的建構式外(指默認建構式和帶其他引數的建構式),不得宣告任何其他的建構式,operator=函式和解構式,
何時自動移動
使用std::move是一種強制的,顯式的移動,但是C++很多時候為了效率會自動幫我們移動,主要的規則其實就是所有的右值都會進行移動,如果不能移動,進行拷貝,但是為了嚴謹,我們還是擺出cppreference上的規則:
- 初始化的時候使用
std::move():T a = std::move(b)或者T a(std::move(b));這種,這里要加上std::move(),不然會呼叫復制建構式, - 函式實參傳遞的時候使用
std::move():func(std::move(a)) - 函式回傳時,如:
class A {};
A CreateA() {
return A();
}
// call
A a = CreateA();
的時候,使用A()產生的變數會首先移動到CreateA()函式產生的回傳值中,這個時候這個回傳值是一個臨時變數(我們記為temp),接下來就是執行這段代碼:A a = temp,然后temp是臨時變數, 會再次呼叫A的移動建構式給a變數,
前兩個是屬于顯式的移動,最后一種就是隱式移動,移動賦值運算子的規則也是一樣,只有等號右邊是臨時變數就會自動呼叫,
復制消除,RVO和NRVO
雖然C++對移動操作定義的很明確,但編譯器卻并不總是按照這個定義去做,因為編譯器中有三個重要的優化經常會減少拷貝,甚至是移動操作,
在GCC和Clang下可以添加-fno-elide-constructors選項來關閉這三種優化,
復制消除
來看一看下面代碼:
class C {
public:
C() {}
C(const C&) { std::cout << "A copy was made.\n"; }
C(C&& rhs) { std::cout << "A move was made.\n"; }
};
C f() {
return C();
}
int main() {
std::cout << "Hello World!\n";
C obj = f();
}
這里建議在C++17標準下編譯,因為C++17起所有的復制消規則除被寫在語言規范內,大部分編譯器應該都會做這件事,我的Clang++ 12.0.5上的執行結果僅僅是輸出了一行Hello World:
Hello World!
按照上面的規則,函式在回傳的時候會進行移動,也就是說在f()的呼叫內,會先移動給臨時變數,然后臨時變數再移動給obj,但是這里什么都沒發生,沒有任何的移動和拷貝,obj就像憑空出現了一樣,
在C++17起,復制消除是強制執行的,而C++11中是看編譯器心情,
在如下條件下會進行復制消除:
- 在return陳述句中,return的值是和函式回傳值型別一樣的右值,型別一樣是為了防止隱式轉換,否則會產生新的變數從而阻止移動,右值是因為C++自動移動只能對右值操作,
- 在變數初始化的時候,初始化運算式是右值,如:
class A{};
A f() { return A(); } // 這里是第一種情況,會自動復制消除
// call
A a = f(); // 這里函式回傳值的臨時變數到a的程序中的移動也會被消除
這也就解釋了為什么上面的代碼沒有呼叫任何的拷貝,移動函式了,
RVO和NRVO
RVO是Return Value Optimization(回傳值優化)的簡寫,而NRVO是Named Return Value Optimization(命名回傳值優化)的簡寫,這兩個優化是復制消除的常見形式,
通過他們的名字就可以看出,這是在函式回傳的時候做的優化,
RVO是指在函式回傳一個臨時變數時的優化,具體的優化如下:
// 原本的函式
T CreateT(int value) {
return T(value);
}
T a = CreateT(10);
// 優化后的函式(偽代碼):
void CreateT(T& v, int value) {
v.T::T(value); // 直接在內部進行構造
}
即通過將要接收函式回傳值的物件以參考的形式放入函式內部初始化,這樣就避免了一次移動/拷貝,
而NRVO則是更加寬泛的RVO,對于如下的代碼可以執行NRVO:
T CreateT(int values) {
T t(value);
return t;
}
編譯器也會優化成上面RVO優化的樣子,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/295925.html
標籤:C++
上一篇:學習筆記--cdq
