web-dev-qa-db-ja.com

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

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

それは次のようなものについて何が変わりますか

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

そして

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

または

f(g(),h(),j());
76
Johan Lundberg

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

のようなものはどうですか

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

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

ただし、各引数の評価は、requiredであり、他の引数を実行する前に、すべての副作用を伴って完全に実行されます。したがって、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, "");
_

この例は、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. a.b
  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)と誤解されました。 _b1_、_b2_、_b3_の順序はまだ指定されていません。 (@KABoissonneault、すべてのコメント者に感謝します。)

ただし、(@ Yakkが指摘しているように)これは重要です:_b1_、_b2_、_b3_が非自明な式である場合でも、それぞれが完全に評価されます他のパラメータの評価が開始される前に、それぞれの関数パラメータに関連付けられます。標準では次のように記述されています。

§5.2.2-関数呼び出し5.2.2.4:

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

ただし、これらの新しい文の1つが github draft にありません。

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

isがあります。何十年も前の問題を解決します( Herb Sutterによる説明

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

f(get_raw_a(),get_raw_a()); 
_

呼び出しget_raw_a()の1つが、他の生のポインターがそのスマートポインターパラメーターに関連付けられる前にスローする場合、リークします。 編集:T.C.が指摘したとおりrawポインターからのunique_ptrの構築は明示的であり、コンパイルを妨げるため、この例には欠陥があります。

また、この古典的な question (tagged[〜#〜] c [〜#〜]ではなく、C++)にも注意してください。

_int x=0;
x++ + ++x;
_

未定義です。

64
Johan Lundberg

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の前にシーケンスされます。

その文には次のような脚注があります。

つまり、関数の実行は互いにインターリーブしません。

これにより、2つの有効な順序((1)、(2)、(3)、(4)または(3)、(4)、(1)、(2))が残ります。どの順序を使用するかは指定されていませんが、これらは両方とも安全です。 (1)(3)が(2)と(4)の前に発生するすべての順序付けは現在禁止されています。

35
Barry

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

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

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

  • 評価の順序

    21)括弧で囲まれた初期化子の式のコンマ区切りリスト内のすべての式は、関数呼び出しの場合と同様に評価されます(indeterminately-sequenced

  • あいまいな表現

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

In P0145R3。慣用的なC++の式評価順序の改良 見つけました:

Postfix-expressionの値の計算と関連する副作用は、expression-list内の式の値の前にシーケンスされます。宣言されたパラメータの初期化は、不定にシーケンスされます、インターリーブはありません。

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

6.8.1.8順次実行[intro.execution] 式Xは、式Xに関連付けられたすべての値計算と副作用がすべての値計算とすべての前にシーケンスされる場合、式Yの前にシーケンスされる式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>
1
lvccgd