簡體   English   中英

賦值運算符中奇怪的 C++14 和 C++17 差異

[英]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。 所以,我有以下問題找不到答案:

  • C++17 是否在評估賦值表達式的 RHS 后評估a[0]的內存地址? C++14 之前是否評估過這一點,這就是它改變的原因?
  • 這是在 C++17 中修復的錯誤嗎? 標准有什么變化?
  • 使用 C++14 或 C++11 時,是否有一種干凈的方法可以使此賦值的行為類似於 C++17?

由於C++ 評估順序規則,此代碼是 C++17 之前的 UB,如注釋中所指出的。 基本問題:操作順序不是求值順序。 甚至像x++ + x++這樣的東西也是 UB。

在 C++17 中,賦值的排序規則發生了變化:

  1. 在每個簡單賦值表達式 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++這樣的表達式本質上是未定義的,因為在一般情況下變量是不同的(比如ij ),編譯器可以重新排序指令以制作更高效的代碼,並且編譯器不必考慮變量的特殊情況在這種情況下,生成的代碼可能會產生不同的結果,具體取決於它的實現方式。

大多數原始規則基於不支持運算符重載的 C。

然而,對於用戶定義的類型,這種靈活性並不總是可取的,因為有時它會為看起來正確的代碼提供錯誤的代碼(如我上面的簡化f函數)。

因此,更精確地定義規則以解決此類不良行為。 較新的規則也適用於預定義類型(如int運算符。

由於永遠不應該編寫依賴於未定義行為的代碼,因此在 C++11 之前編寫的任何有效程序在更嚴格的 C++11 規則下也是有效的。C++17 也是如此。

另一方面,如果您的程序是使用 C++17 規則編寫的,那么如果您使用較舊的編譯器進行編譯,它可能會有未定義的行為。

通常,人們會在不必返回到舊編譯器的時候開始使用新規則進行編寫。

顯然,如果一個人為其他人編寫庫,那么他需要確保他的代碼沒有支持的 C++ 標准的未定義行為。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM