Visual Studio新版本兩項改變

roc_guo發表於2022-11-27

Visual Studio新版本兩項改變Visual Studio新版本兩項改變

蠍子

為了能發文,標題中的複製/移動省略是 Copy/Move Elision 的硬翻譯,請各位大大海涵。下文中我會同時使用這兩種術語。

Visual Studio 中 Copy/Move Elision 的變化

在 Visual Studio 2022 版本 17.4 預覽版 3 中,我們顯著增加了適用於Copy/Move Elision 情況的數量,並讓使用者能夠更好地控制是否啟用這些轉換。

Copy/Move Elision 是什麼?

當 C++ 函式中的 return 關鍵字後跟非內建型別的表示式時,執行該 return 語句會將表示式的結果複製到呼叫函式的返回槽(Return Slot)中。為此,將呼叫非內建型別的複製或移動建構函式。然後,作為退出函式的一部分,將呼叫函式區域性變數的解構函式,可能包括 return 關鍵字後面的表示式中命名的任何變數。

C++ 規範允許編譯器直接在呼叫函式的返回槽中構造返回的物件,從而省略作為返回的一部分執行的複製或移動建構函式。與大多數其他最佳化不同,這種轉換允許對程式的輸出產生可觀察的影響 – 即複製或移動建構函式以及關聯的解構函式可以少呼叫一次。

Visual Studio 中的 Copy/Move Elision

C++ 標準要求在將返回值初始化為 return 語句的一部分時(例如,當返回型別為 Foo 的函式返回返回 Foo()時),編譯器需要執行 Copy/Move Elision。Microsoft Visual C++ 編譯器始終根據需要對返回語句執行 Copy/Move Elision,而不管傳遞給編譯器的標誌如何。此行為保持不變。

在 Visual Studio 17.4 預覽版 3 中對可選 Copy/Move Elision 的更改

當返回的值為命名變數時,編譯器可能會省略複製或移動,但不是必需的。C++ 標準仍要求為命名的返回變數定義複製或移動建構函式,即使編譯器在所有情況下都省略了建構函式。在 Visual Studio 2022 版本 17.4 預覽版 3 之前,當禁用最佳化(例如使用 /Od 編譯器標誌或使用了 #pragma optimize(“”,off))時,編譯器將僅執行強制Copy/Move Elision。使用 /O2 標誌,編譯器將透過簡單的控制流為最佳化的函式執行可選的Copy/Move Elision。

從 Visual Studio 2022 版本 17.4 預覽版 3 開始,我們為開發人員提供了與新的 /Zc:nrvo 編譯器標誌保持一致的選項。預設情況下,當使用 /O2 標誌、/permissive- 編譯程式碼時,或者在為 /std:c++20 或更高版本進行編譯時,將傳遞 /Zc:nrvo 標誌。透過此標誌後,將盡可能執行復制和移動省略。我們希望在將來的版本中預設啟用 /Zc:nrvo。另外,開發者還可以使用 /Zc:nrvo- 標誌顯式禁用可選的Copy/Move Elision。請注意,無法禁用強制型的Copy/Move Elision。

在 Visual Studio 2022 版本 17.4 預覽版 3 中,當使用 /Zc:nrvo、/O2、/permissive-或 /std:c++20 或更高版本的標誌啟用可選複製/移動省略時,我們還增加了Copy/Move Elision的位置。

可選 Copy/Move Elision 的示例

可選 Copy/Move Elision 的最簡單示例是以下函式:Foo SimpleReturn() {Foo result;return result;}

在這種情況下,如果傳遞了 /O2 標誌,則早期版本的 MSVC 編譯器已將結果的複製或移動到返回槽中。在 Visual Studio 2022 版本 17.4 預覽版 3 中,如果傳遞了 /permissive-、/std:c++20 或更高版本或 /Zc:nrvo 標誌,也會省略複製或移動,如果傳遞了 /Zc:nrvo- 標誌,則保留複製或移動。

從 Visual Studio 2022 版本 17.4 預覽版 3 開始,如果將 /O2、/permissive-、/std:c++20 或更高版本或 /Zc:nrvo 標誌傳遞給編譯器,而 /Zc:nrvo- 標誌未傳遞到編譯器,我們現在在以下其他情況下執行復制/移動省略。

在迴圈中返回

Foo ReturnInALoop(int iterations) {for (int i = 0; i < iterations; ++i) {Foo result;if (i == (iterations / 2)) {return result;}}}結果物件將在迴圈的每次迭代開始時正確構造,並在每次迭代結束時銷燬。在返回結果的迭代中,退出函式時不會呼叫其解構函式。當返回的物件超出該函式的範圍時,函式的呼叫方將銷燬該物件。 [yiji]在異常處理中返回[/yiji]

Foo ReturnInTryCatch() {
try {
Foo result;
return result;
} catch (…) {}
}

如果傳遞了 /O2、/permissive-、/std:c++20 或更高版本,或者傳遞了 /Zc:nrvo 標誌,而 /Zc:nrvo- 標誌未傳遞,則結果物件的複製或移動現在將被省略。我們現在還可以妥善處理更復雜的情況,例如:

int n;
void throwFirstThreeIterations() {
++n;
if (n <= 3) throw n;
}
Foo ComplexTryCatch()
{
Label1:
Foo result;
try {
throwFirstThreeIterations();
return result;
}
catch(…) {
goto Label1;
}
}

結果物件將在呼叫方函式的返回槽中構造,並且在成功返回時不會為其呼叫複製/移動建構函式或解構函式。引發異常時,是否析構結果物件取決於向編譯器傳遞哪些異常處理標誌。預設情況下,不會發生堆疊展開,因此不會呼叫解構函式。但是,如果使用 /EHs、/EHa 或 /EHr 標誌啟用了堆疊展開異常處理,則 goto Label1 將導致呼叫結果的解構函式,因為它跳轉到初始化結果之前。無論哪種方式,當再次到達表示式 Foo 結果時,將在返回槽中再次構造物件。

複製具有預設引數的建構函式

現在,我們可以正確檢測到具有預設引數的複製或移動建構函式仍然是複製或移動建構函式,因此可以在上述情況下被省略。具有預設引數的複製建構函式如下所示:structStructWithCopyConstructorDefaultParam {int X;

struct
StructWithCopyConstructorDefaultParam {
int X;
StructWithCopyConstructorDefaultParam(int x) : X(x) {}
StructWithCopyConstructorDefaultParam(StructWithCopyConstructorDefaultParam const& original, int defaultParam = 0) :
X(original.X + defaultParam) {
printf(“Copy constructor called.\n”);
}
};
對NRVO的限制

儘管 MSVC 編譯器現在在更多情況下執行Copy/Move Elision,但並不總是能夠執行它。若要了解為什麼會這樣,請考慮以下函式:

Foo WhichShouldIReturn(bool condition) {
Foo resultA;
if (condition) {
Foo resultB;
return resultB;
}
return resultA;
}

複製省略構造要在返回槽中返回的物件,但在這種情況下,應在返回槽中構造哪個物件?為了在返回結果A時省略結果A的副本,必須在返回槽中構造它。但是,如果條件為真,則需要在銷燬結果 A 之前在返回槽中構造結果 B。無法對兩個路徑執行復制省略。

我們目前選擇避免在函式中的所有路徑上執行可選的Copy/Move Elision,如果在任何路徑上它是不可能的的話。但是,對內聯決策、死程式碼消除和其他最佳化的更改可能會更改Copy/Move Elision的可能性。因此,編寫依賴於命名變數的Copy/Move Elision的某些行為的程式碼是不安全的,除非使用 /Zc:nrvo- 禁用了所有可選的Copy/Move Elision。

只要啟用了堆疊展開異常處理或未引發異常,仍然可以安全地假定每個建構函式呼叫都有匹配的解構函式呼叫。

總結

寫著舊時代的 C++,一直都為如何高效能地返回一個物件發愁。沒錯,正是在下。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69901823/viewspace-2925339/,如需轉載,請註明出處,否則將追究法律責任。

相關文章