簡體   English   中英

C++17 引入的求值順序保證是什么?

[英]What are the evaluation order guarantees introduced by C++17?

C++17 評估順序保證 (P0145) 中投票對典型 C++ 代碼有什么影響?

以下內容有什么變化?

i = 1;
f(i++, i)

std::cout << f() << f() << f();

或者

f(g(), h(), j());

到目前為止尚未指定求值順序的一些常見情況在C++17已指定且有效。 一些未定義的行為現在改為未指定。

 i = 1; f(i++, i)

未定義,但現在未指定。 具體來說,未指定的是f每個參數相對於其他參數的評估順序。 i++可能在i之前被評估,反之亦然。 事實上,盡管在同一個編譯器下,它可能會以不同的順序評估第二個調用。

但是,在執行任何其他參數之前,需要對每個參數的評估完全執行,並產生所有副作用。 因此,您可能會得到f(1, 1) (首先評估第二個參數)或f(1, 2) (首先評估第一個參數)。 但是你永遠不會得到f(2, 2)或任何其他性質的東西。

 std::cout << f() << f() << f();

未指定,但它將與運算符優先級兼容,因此f的第一個評估將首先出現在流中(下面的示例)。

 f(g(), h(), j());

仍然有未指定的 g、h 和 j 的評估順序。 請注意,對於getf()(g(),h(),j()) ,規則規定getf()將在g, h, j之前進行評估。

另請注意提案文本中的以下示例:

 std::string s = "but I have heard it works even if you don't believe in it" s.replace(0, 4, "").replace(s.find("even"), 4, "only") .replace(s.find(" don't"), 6, "");

該示例來自The C++ Programming Language ,第 4 版,Stroustrup,並且曾經是未指定的行為,但使用 C++17 它將按預期工作。 可恢復函數也有類似的問題( .then( . . . ) )。

作為另一個示例,請考慮以下內容:

#include <iostream>
#include <string>
#include <vector>
#include <cassert>

struct Speaker{
    int i =0;
    Speaker(std::vector<std::string> words) :words(words) {}
    std::vector<std::string> words;
    std::string operator()(){
        assert(words.size()>0);
        if(i==words.size()) i=0;
        // Pre-C++17 version:
        auto word = words[i] + (i+1==words.size()?"\n":",");
        ++i;
        return word;
        // Still not possible with C++17:
        // return words[i++] + (i==words.size()?"\n":",");

    }
};

int main() {
    auto spk = Speaker{{"All", "Work", "and", "no", "play"}};
    std::cout << spk() << spk() << spk() << spk() << spk() ;
}

使用 C++14 和之前我們可能(並且將會)得到諸​​如

play
no,and,Work,All,

代替

All,work,and,no,play

請注意,上述內容實際上與

(((((std::cout << spk()) << spk()) << spk()) << spk()) << spk()) ;

但是,在 C++17 之前,不能保證第一個調用會首先進入流。

參考文獻:來自接受的提案

后綴表達式從左到右計算。 這包括函數調用和成員選擇表達式。

賦值表達式從右到左計算。 這包括復合賦值。

移位運算符的操作數從左到右計算。 總之,以下表達式的計算順序是 a,然后是 b,然后是 c,然后是 d:

  1. AB
  2. a->b
  3. a->*b
  4. a(b1, b2, b3)
  5. b @= a
  6. [b]
  7. 一<<b
  8. 一 >> 乙

此外,我們建議以下附加規則:涉及重載運算符的表達式的計算順序由與相應內置運算符關聯的順序決定,而不是由函數調用規則決定。

編輯說明:我的原始答案誤解a(b1, b2, b3) b1b2b3的順序仍未指定。 (感謝@KABOissonneault,所有評論者。)

但是,(正如@Yakk 指出的那樣)這很重要:即使b1b2b3是非平凡的表達式,它們中的每一個也會被完全評估在開始評估其他函數參數之前與各自的函數參數相關聯 標准是這樣規定的:

§5.2.2 - 函數調用 5.2.2.4:

. . . 后綴表達式排在表達式列表中的每個表達式和任何默認參數之前。 與參數初始化相關的每個值計算和副作用,以及初始化本身,都在與任何后續參數初始化相關的每個值計算和副作用之前進行排序。

但是, GitHub 草案中缺少以下新句子之一:

與參數初始化相關的每個值計算和副作用,以及初始化本身,都在與任何后續參數初始化相關的每個值計算和副作用之前進行排序。

例子在那里。 它解決了幾十年前的問題( 如 Herb Sutter 所解釋的),並具有異常安全性,例如

f(std::unique_ptr<A> a, std::unique_ptr<B> b);

f(get_raw_a(), get_raw_a());

如果其中一個調用get_raw_a()在另一個原始指針綁定到其智能指針參數之前拋出,則會泄漏。

正如 TC 所指出的,該示例存在缺陷,因為從原始指針構建的 unique_ptr 是顯式的,從而阻止了編譯。*

還要注意這個經典問題(標記為C ,而不是C++ ):

 int x=0; x++ + ++x;

仍然未定義。

C++17 中禁止交錯

在 C++14 中,以下是不安全的:

void foo(std::unique_ptr<A>, std::unique_ptr<B>);

foo(std::unique_ptr<A>(new A), std::unique_ptr<B>(new B));

在函數調用過程中,這里發生了四個操作

  1. new A
  2. unique_ptr<A>構造函數
  3. new B
  4. unique_ptr<B>構造函數

這些的順序是完全未指定的,因此完全有效的順序是 (1)、(3)、(2)、(4)。 如果選擇了這個排序並且 (3) 拋出,那么來自 (1) 的內存泄漏——我們還沒有運行 (2),這將防止泄漏。


在 C++17 中,新規則禁止交錯。 來自 [intro.execution]:

對於每個函數調用 F,對於在 F 中發生的每個求值 A 和不在 F 中但在同一線程上作為同一信號處理程序(如果有)的一部分求值的每個求值 B,A 都在 B 之前排序或 B 在 A 之前排序。

這句話有一個腳注,內容如下:

換句話說,函數執行不會相互交錯。

這給我們留下了兩個有效的排序:(1)、(2)、(3)、(4) 或 (3)、(4)、(1)、(2)。 未指定采用哪種排序方式,但這兩種方式都是安全的。 現在禁止 (1) (3) 都發生在 (2) 和 (4) 之前的所有排序。

我發現了一些關於表達式求值順序的注釋:

  • 快速問:為什么 c++ 沒有指定的順序來評估函數參數?

    某些求值順序保證了 C++17 中添加的重載運算符和完整參數規則。 但仍然沒有指定哪個參數先進行。 在 C++17 中,現在指定給出調用內容的表達式(函數調用左側的代碼)在參數之前,並且首先計算的參數在下一個參數之前完全計算開始,並且在對象方法的情況下,對象的值在方法的參數之前被評估。

  • 評估順序

    21) 括號中的初始值設定項中以逗號分隔的表達式列表中的每個表達式都像函數調用一樣求值(不確定順序

  • 含糊不清的表達

    C++ 語言不保證對函數調用的參數求值的順序。

P0145R3.Refining Expression Evaluation Order for Idiomatic C++我發現:

后綴表達式的值計算和相關副作用排在表達式列表中的表達式之前。 聲明參數的初始化是不確定的,沒有交錯。

但是我沒有在標准中找到它,而是在我發現的標准中:

6.8.1.8 順序執行 [intro.execution]如果每次值計算和與表達式 X 關聯的每個副作用都在每次值計算和與表達式 Y 關聯的每個副作用之前排序,則稱表達式 X 在表達式 Y 之前排序.

6.8.1.9 順序執行 [intro.execution]與完整表達式相關的每個值計算和副作用在與要評估的下一個完整表達式相關的每個值計算和副作用之前被排序。

7.6.19.1 逗號運算符 [expr.comma] 從左到右計算一對由逗號分隔的表達式;...

因此,我比較了 14 和 17 標准的三個編譯器的相應行為。 探索的代碼是:

#include <iostream>

struct A
{
    A& addInt(int i)
    {
        std::cout << "add int: " << i << "\n";
        return *this;
    }

    A& addFloat(float i)
    {
        std::cout << "add float: " << i << "\n";
        return *this;
    }
};

int computeInt()
{
    std::cout << "compute int\n";
    return 0;
}

float computeFloat()
{
    std::cout << "compute float\n";
    return 1.0f;
}

void compute(float, int)
{
    std::cout << "compute\n";
}

int main()
{
    A a;
    a.addFloat(computeFloat()).addInt(computeInt());
    std::cout << "Function call:\n";
    compute(computeFloat(), computeInt());
}

結果(更一致的是clang):

 <style type="text/css"> .tg { border-collapse: collapse; border-spacing: 0; border-color: #aaa; } .tg td { font-family: Arial, sans-serif; font-size: 14px; padding: 10px 5px; border-style: solid; border-width: 1px; overflow: hidden; word-break: normal; border-color: #aaa; color: #333; background-color: #fff; } .tg th { font-family: Arial, sans-serif; font-size: 14px; font-weight: normal; padding: 10px 5px; border-style: solid; border-width: 1px; overflow: hidden; word-break: normal; border-color: #aaa; color: #fff; background-color: #f38630; } .tg .tg-0pky { border-color: inherit; text-align: left; vertical-align: top } .tg .tg-fymr { font-weight: bold; border-color: inherit; text-align: left; vertical-align: top } </style> <table class="tg"> <tr> <th class="tg-0pky"></th> <th class="tg-fymr">C++14</th> <th class="tg-fymr">C++17</th> </tr> <tr> <td class="tg-fymr"><br>gcc 9.0.1<br></td> <td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td> <td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td> </tr> <tr> <td class="tg-fymr">clang 9</td> <td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute float<br>compute int<br>compute</td> <td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute float<br>compute int<br>compute</td> </tr> <tr> <td class="tg-fymr">msvs 2017</td> <td class="tg-0pky">compute int<br>compute float<br>add float: 1<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td> <td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td> </tr> </table>

暫無
暫無

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

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