[英]Weird C++14 and C++17 difference in assignment operator
我有以下代碼:
#include <vector>
#include <iostream>
std::vector <int> a;
int append(){
a.emplace_back(0);
return 10;
}
int main(){
a = {0};
a[0] = append();
std::cout << a[0] << '\n';
return 0;
}
函數append()
作為副作用將向量大小增加一。 由於向量的工作方式,這會在超出其容量時觸發其內存的重新分配。
因此,在執行a[0] = append()
,如果發生重新分配,則a[0]
無效並指向向量的舊內存。 因此,您可以期望向量最終為{0, 0}
而不是{10, 0}
,因為它分配給舊的a[0]
而不是新的。
令我困惑的奇怪之處在於,這種行為在 C++14 和 C++17 之間發生了變化。
在 C++14 上,程序將打印 0。在 C++17 上,它將打印 10,這意味着a[0]
實際上分配給它 10。 所以,我有以下問題找不到答案:
a[0]
的內存地址? C++14 之前是否評估過這一點,這就是它改變的原因?由於C++ 評估順序規則,此代碼是 C++17 之前的 UB,如注釋中所指出的。 基本問題:操作順序不是求值順序。 甚至像x++ + x++
這樣的東西也是 UB。
在 C++17 中,賦值的排序規則發生了變化:
- 在每個簡單賦值表達式 E1=E2 和每個復合賦值表達式 E1@=E2 中,E2 的每個值計算和副作用都在 E1 的每個值計算和副作用之前排序
程序之前,C ++ 17具有未定義的行為,因為所述評價順序是不確定的,並且可能的選擇導致使用作廢的參考中的一個。 (這就是未定義行為的工作原理:即使您先記錄兩個評估和右側的記錄,它也可能以另一種方式評估,未定義行為的影響是不正確的記錄。)
雖然這是一個非正式的觀點,但在 C++17 中為某些運算符(包括=
)指定計算順序的更改不被認為是錯誤修復,這就是編譯器不在先前語言中實現新規則的原因模式。 (損壞的是代碼,而不是語言。)
清潔度是主觀的,但處理這樣的排序問題的常用方法是引入一個臨時變量:
{ // limit scope
auto x=append();
a[0]=x; // with std::move for non-trivial types
}
這偶爾會干擾將值傳遞給賦值運算符,這是無濟於事的。
雖然現在在更多情況下指定了規則,但只有當新規則使代碼更易於理解或更高效或更正確時,才應依賴新規則。
您的代碼有很多問題:
更多細節
眾所周知,應該避免使用全局變量。 這甚至是最糟糕的,因為您的變量位於單個字母名稱 ( a
) 中,這使得它容易發生名稱沖突。
append
函數做了兩件事。 它附加值並返回一個不相關的值。 最好有 2 個獨立的功能:
void append_0_to_a()
{
a.emplace_back(0);
}
// I have no idea what 10 represent in your code so I make a guess
int get_number_of_fingers()
{
const int hands = 2;
const int fingerPerHands = 5;
return hands * fingerPerHands;
}
通過編寫主代碼將更具可讀性
a = { 0 };
append_0_to_a();
a[0] = get_number_of_fingers();
但即便如此,也不清楚為什么要使用如此多的語法來修改向量。 為什么不簡單地寫一些類似的東西
int main()
{
std::vector <int> a = { get_number_of_fingers(), 0 };
std::cout << a[0] << '\n';
return 0;
}
通過編寫更清晰的代碼,您不需要了解評估順序的高級知識,其他閱讀您代碼的人會更容易理解它。
雖然評估規則主要在 C++ 11 和 C++ 17 中更新,但不應濫用這些規則來編寫難以閱讀的代碼。
改進了規則,使代碼在某些情況下可見,例如當函數在參數中接收多個std::unique_ptr
時。 通過強制編譯器在評估另一個參數之前完全評估一個參數,它將使代碼異常安全(無內存泄漏)在這樣的情況下:
// Just a simplified example --- not real code
void f(std::unique_ptr<int> a, std::unique_ptr<int> b)
{
...
}
f(new 2, new 3);
較新的規則確保在 new 調用另一個參數之前調用一個參數的std::unique_ptr
構造函數,從而防止在第二次調用new
拋出異常時可能發生的泄漏。
較新的規則還確保了一些對用戶定義的函數和運算符很重要的排序,以便鏈接調用是直觀的。 據我所知,這對於諸如改進的future
和其他asynch
內容的庫很有用,因為在舊規則中存在太多未定義或未指定的行為。
正如我在評論中提到的,原始規則允許編譯器進行更積極的優化。
像i++ + i++
這樣的表達式本質上是未定義的,因為在一般情況下變量是不同的(比如i
和j
),編譯器可以重新排序指令以制作更高效的代碼,並且編譯器不必考慮變量的特殊情況在這種情況下,生成的代碼可能會產生不同的結果,具體取決於它的實現方式。
大多數原始規則基於不支持運算符重載的 C。
然而,對於用戶定義的類型,這種靈活性並不總是可取的,因為有時它會為看起來正確的代碼提供錯誤的代碼(如我上面的簡化f
函數)。
因此,更精確地定義規則以解決此類不良行為。 較新的規則也適用於預定義類型(如int
運算符。
由於永遠不應該編寫依賴於未定義行為的代碼,因此在 C++11 之前編寫的任何有效程序在更嚴格的 C++11 規則下也是有效的。C++17 也是如此。
另一方面,如果您的程序是使用 C++17 規則編寫的,那么如果您使用較舊的編譯器進行編譯,它可能會有未定義的行為。
通常,人們會在不必返回到舊編譯器的時候開始使用新規則進行編寫。
顯然,如果一個人為其他人編寫庫,那么他需要確保他的代碼沒有支持的 C++ 標准的未定義行為。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.