1. ホーム
  2. c++

[解決済み] C++17で導入された評価順序の保証とは何ですか?

2023-01-05 09:07:16

質問

での投票の意味は何ですか? 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 の前に評価されるかもしれませんし、その逆もありえます。実際、同じコンパイラであるにもかかわらず、異なる順序で2回目の呼び出しを評価する可能性があります。

しかし、各引数の評価は が必要です。 で、他のどの引数の実行の前にも、すべての副作用を含めて完全に実行される必要があります。ですから、次のようになります。 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, "");

この例は 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以前は、最初の呼び出しがストリームに最初に来るという保証はありませんでした。

参考文献 から 採択された提案 :

<ブロッククオート

Postfix の式は左から右へ評価されます。これには 関数の呼び出しやメンバー選択式も含まれます。

代入式は右から左へ評価されます。これには 複合代入も含まれます。

シフト演算子へのオペランドは、左から右へ評価されます。つまり 要約すると、次の式は a、b、c、d の順で評価されます。 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 のドラフト :

パラメータの初期化に関連するすべての値の計算と副作用、そして初期化そのものが パラメータの初期化、および初期化そのものに関連するすべての値の計算と副作用は 後続のパラメータの初期化に関連するすべての値の計算と副作用の前にシーケンスされます。 その後に続くパラメーターの初期化に関連するすべての値の計算と副作用の前に順序付けられます。

例として があります。これは何十年も前の問題を解決するものです ( は、Herb Sutterによって説明されたように ) のような例外安全性を解決します。

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

f(get_raw_a(), get_raw_a());

の呼び出しのいずれかがあると、リークします。 get_raw_a() が投げる前に、もう一方の がスマートポインタパラメータに結びつけられる前に投げられるとリークします。

T.C.によって指摘されたように、生ポインタからの unique_ptr 構築が明示的であるため、この例は欠陥があり、これがコンパイルから防止されます*。

また、この古典的な 質問 (タグ付けされた C ではなく C++ ):

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

はまだ未定義です。