C ++ 17によって導入された評価順序保証とは何ですか?


95

典型的なC ++コードでのC ++ 17評価順序保証(P0145)での投票の意味は何ですか?

次のようなことで何が変わりますか?

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

そして

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

または

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

関連したC ++での代入文の評価の順序ない「C ++プログラミング言語」第4版セクション36.3.6から、このコードは、明確に定義された振る舞いを持っていますか?どちらも紙で覆われています。最初のものは、以下のあなたの答えに良い追加の例を作るかもしれません。
Shafik Yaghmour 2017年

また、いくらか関連しています:演算子オーバーロード関数を使用したc ++ 17評価順序
dfrib 2017

回答:


83

評価順序がこれまで指定されていないいくつかの一般的なケースは、で指定され、有効ですC++17。一部の未定義の動作は、代わりに未指定になりました。

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

は未定義でしたが、現在は指定されていません。具体的には、指定されていないのは、への各引数fが他の引数と比較して評価される順序です。i++iに評価されるか、またはその逆の場合があります。実際、同じコンパイラーの下にあるにもかかわらず、2番目の呼び出しを異なる順序で評価する場合があります。

ただし、各引数の評価は、他の引数を実行する前に、すべての副作用を含めて完全に実行する必要あります。したがって、f(1, 1)(2番目の引数が最初に評価される)または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, "");

この例は、C ++プログラミング言語、第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. a [b]
  7. a << b
  8. a >> b

さらに、次の追加ルールをお勧めします。オーバーロードされた演算子を含む式の評価の順序は、関数呼び出しのルールではなく、対応する組み込み演算子に関連付けられた順序によって決定されます。

メモの編集:私の元の回答は誤って解釈されましたa(b1, b2, b3)。順序はb1b2b3まだ特定されていません。(@KABoissonneault、すべてのコメント投稿者に感謝します。)

しかし、(@Yakkが指摘するように)、これは重要です:場合でもb1b2b3、非自明表現されているそれらのそれぞれが完全に評価されていると、それぞれの関数のパラメータに結び付けられ、他のものが評価され始めている前に。標準では、次のように述べています。

§5.2.2-関数呼び出し5.2.2.4:

。。。後置式は、式リスト内の各式とデフォルトの引数の前にシーケンスされます。パラメータの初期化に関連するすべての値の計算と副作用、および初期化自体は、後続のパラメータの初期化に関連するすべての値の計算と副作用の前にシーケンスされます。

ただし、これらの新しい文の1つがGitHubドラフトから欠落しています

パラメータの初期化に関連するすべての値の計算と副作用、および初期化自体は、後続のパラメータの初期化に関連するすべての値の計算と副作用の前にシーケンスされます。

そこにあります。それは(ハーブサッターによって説明されているように)数十年前の問題を解決しますが、

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

f(get_raw_a(), get_raw_a());

get_raw_a()他のrawポインターがそのスマートポインターパラメーターに関連付けられる前に、呼び出しの1つがスローされると、リークが発生します。

TCが指摘しているように、rawポインターからのunique_ptr構文が明示的であり、これがコンパイルされないため、この例には欠陥があります。*

また、この古典注意して質問を(タグ付けされたC、いないC ++を):

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

まだ定義されていません。


1
「2番目の補助的な提案は、関数呼び出しの評価順序を次のように置き換えます。関数はすべての引数の前に評価されますが、(引数リストからの)引数のペアは不確定に順序付けられます。つまり、一方が他方の前に評価されますが、順序は指定されていません。引数の前に関数が評価されることが保証されています。これは、コアワーキンググループの一部のメンバーによる提案を反映しています。」
Yakk-Adam Nevraumont 2016

1
「次の式はa、次にb、次にc、次にd」という順序で評価され、次に表示されa(b1, b2, b3)、すべてのb式が必ずしも任意の順序で評価されるとは限らないことを示唆しているという印象を受けています(そうでない場合はa(b, c, d)
KABoissonneault 2016

1
@KABoissoneault、あなたは正しいです、そして私はそれに応じて答えを更新しました。また、すべて:引用フォームバージョン3であり、私が理解している限り、バージョンで投票されています。
Johan Lundberg 2016

2
@JohanLundberg論文から、私が重要だと信じている別のことがあります。 a(b1()(), b2()())注文することはできませんb1()()し、b2()()任意の順序で、それはできません行いb1()、その後b2()()、次にb1()():それはもはや彼らの処刑インターリーブがあります。要するに、「8。機能呼び出しの代替評価命令」は承認された変更の一部でした。
Yakk-Adam Nevraumont 2016

3
f(i++, i)未定義でした。現在は指定されていません。Stroustrupの文字列の例は、おそらく未定義ではなく、未指定でした。`f(get_raw_a()、get_raw_a());`は、関連するunique_ptrコンストラクターが明示的であるため、コンパイルされません。最後に、x++ + ++x未定義の期間です。
TC

44

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));

関数呼び出し中にここで発生する4つの操作があります

  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)の2つの有効な順序が残ります。どちらの注文をするかは定かではありませんが、どちらも安全です。(1)(3)が(2)と(4)の前に発生するすべての注文は現在禁止されています。


1
少し脇に置いておきますが、これがboost :: make_sharedの理由のひとつであり、後でstd :: make_sharedになりました(他の理由は、割り当てが少なく、局所性が高いことです)。例外安全性/リソースリークの動機はもはや適用されないように聞こえます。参照してくださいコード例3、boost.org/doc/libs/1_67_0/libs/smart_ptr/doc/html/... 編集およびstackoverflow.com/a/48844115herbsutter.com/2013/05/29/gotw-89-solution-スマートポインター
Max Barraclough 2018

3
この変更が最適化にどのように影響するのだろうか。コンパイラは、引数の計算に関連するCPU命令を組み合わせてインターリーブする方法に関するオプションの数を大幅に減らしたため、CPU使用率が低下する可能性がありますか?
バイオレットキリン

2

式の評価順序に関するメモをいくつか見つけました。

  • クイックQ:関数の引数を評価するためにc ++に指定された順序がないのはなぜですか?

    評価の順序によっては、C ++ 17で追加された場合、オーバーロードされた演算子と完全な引数ルールを取り巻くことが保証されます。しかし、どちらの議論が最初に進むかは特定されていないままです。C ++ 17では、何を呼び出すかを示す式(関数呼び出しの(の左側のコード)が引数の前にあり、最初に評価された引数が次の引数の前に完全に評価されるように指定されています。開始され、オブジェクトメソッドの場合、メソッドへの引数が行われる前にオブジェクトの値が評価されます。

  • 評価の順序

    21)括弧で囲まれたイニシャライザー内の式のコンマ区切りリスト内のすべての式は、関数呼び出しの場合と同様に評価されます(不確定シーケンス

  • あいまいな表現

    C ++言語は、関数呼び出しへの引数が評価される順序を保証しません。

慣用的なCのために、式の評価順序をP0145R3.Refining ++私が見つけました:

postfix-expressionの値の計算と関連する副作用は、expression-list内の式の値の計算の前にシーケンスされます。宣言されたパラメータの初期化は、インターリーブなしで不確定に順序付けられます

しかし、私はそれを標準で見つけませんでした、代わりに私が見つけた標準で:

6.8.1.8順次実行[intro.execution] 式Xに関連するすべての値の計算とすべての副作用が、式Yに関連するすべての値の計算とすべての副作用の前にシーケンスされる場合、式Xは式Yの前にシーケンスされると言われます。 。

6.8.1.9順次実行[intro.execution] 完全式に関連するすべての値の計算と副作用は、評価される次の完全な式に関連するすべての値の計算と副作用の前にシーケンスされます。

7.6.19.1コンマ演算子[expr.comma] コンマで区切られた式のペアは、左から右に評価されます。

そこで、14標準と17標準の3つのコンパイラの動作を比較しました。調査されたコードは次のとおりです。

#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>

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.