94

Bjarne Stroustrup のThe C++ Programming Language 4th edition セクション36.3.6 STL-like Operationsでは、チェーンの例として次のコードが使用されています。

void f2()
{
    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, "" );

    assert( s == "I have heard it works only if you believe in it" ) ;
}

assert は ( see it live ) と ( see it live ) で失敗しますがgccClangVisual Studio使用する失敗しません( see it live )。

異なる結果が得られるのはなぜですか? これらのコンパイラのいずれかが連鎖式を正しく評価していないか、またはこのコードがなんらかの形式の未指定または未定義の動作を示していますか?

4

2 に答える 2

105

コードは部分式の評価順序が指定されていないため、指定されていない動作を示しますが、すべての副作用は関数内で実行され、この場合、副作用間に順序関係が導入されるため、未定義の動作は発生しません。

この例は、提案N4228: Refining Expression Evaluation Order for Idiomatic C++で言及されており、問題のコードについて次のように述べています。

[...]このコードは、世界中の C++ の専門家によってレビューされ、公開されています (C++ プログラミング言語、4版)。ただし、不特定の評価順序に対する脆弱性がツールによって発見されたのはごく最近のことです[.. .]

詳細

関数への引数の評価順序が指定されていないことは多くの人にとって明らかかもしれませんが、この動作が連鎖関数呼び出しとどのように相互作用するかはおそらくそれほど明白ではありません。このケースを最初に分析したとき、私には明らかではありませんでし

一見すると、それぞれreplaceが左から右に評価されなければならないので、対応する関数引数グループもグループとして左から右に評価されなければならないように見えるかもしれません。

これは正しくありません。関数の引数の評価順序は指定されていません。関数呼び出しの連鎖では、各関数呼び出しに対して左から右への評価順序が導入されますが、各関数呼び出しの引数は、メンバー関数呼び出しに関して前に配列されるだけです。の。特に、これは次の呼び出しに影響します。

s.find( "even" )

と:

s.find( " don't" )

以下に関して不定に順序付けられます。

s.replace(0, 4, "" )

2 つのfind呼び出しは の前または後に評価できます。これは、 の結果を変更するreplace副作用があり、 の長さが変更されるため重要です。そのため、2 つの呼び出しに対していつ評価されるかによって、結果は異なります。sfindsreplacefind

チェーン式を見て、いくつかのサブ式の評価順序を調べると、次のようになります。

s.replace(0, 4, "" ).replace( s.find( "even" ), 4, "only" )
^ ^       ^  ^  ^    ^        ^                 ^  ^
A B       |  |  |    C        |                 |  |
          1  2  3             4                 5  6

と:

.replace( s.find( " don't" ), 6, "" );
 ^        ^                   ^  ^
 D        |                   |  |
          7                   8  9

4and7をさらにサブ式に分解できるという事実を無視していることに注意してください。そう:

  • ABにシーケンスされます前にシーケンスされます 前CにシーケンスされますD
  • 1to9は、以下にリストされているいくつかの例外を除いて、他の部分式に関して不定に順序付けられます
    • 13は前に配列されるB
    • 46は前に配列されるC
    • 79は前に配列されるD

この問題の鍵は次のとおりです。

  • 49に関して不定に順序付けられるB

4に対すると7に関する評価選択の潜在的な順序は、と を評価するときBの結果の違いを説明しclangます。私のテストでは、評価前に評価し、評価後に評価します。次のテスト プログラムを使用して、それぞれのケースで何が起こっているかを示すことができます。gccf2()clangB47gcc

#include <iostream>
#include <string>

std::string::size_type my_find( std::string s, const char *cs )
{
    std::string::size_type pos = s.find( cs ) ;
    std::cout << "position " << cs << " found in complete expression: "
        << pos << std::endl ;

    return pos ;
}

int main()
{
   std::string s = "but I have heard it works even if you don't believe in it" ;
   std::string copy_s = s ;

   std::cout << "position of even before s.replace(0, 4, \"\" ): " 
         << s.find( "even" ) << std::endl ;
   std::cout << "position of  don't before s.replace(0, 4, \"\" ): " 
         << s.find( " don't" ) << std::endl << std::endl;

   copy_s.replace(0, 4, "" ) ;

   std::cout << "position of even after s.replace(0, 4, \"\" ): " 
         << copy_s.find( "even" ) << std::endl ;
   std::cout << "position of  don't after s.replace(0, 4, \"\" ): "
         << copy_s.find( " don't" ) << std::endl << std::endl;

   s.replace(0, 4, "" ).replace( my_find( s, "even" ) , 4, "only" )
        .replace( my_find( s, " don't" ), 6, "" );

   std::cout << "Result: " << s << std::endl ;
}

の結果gccライブで見る

position of even before s.replace(0, 4, "" ): 26
position of  don't before s.replace(0, 4, "" ): 37

position of even after s.replace(0, 4, "" ): 22
position of  don't after s.replace(0, 4, "" ): 33

position  don't found in complete expression: 37
position even found in complete expression: 26

Result: I have heard it works evenonlyyou donieve in it

結果clang(ライブで見る):

position of even before s.replace(0, 4, "" ): 26
position of  don't before s.replace(0, 4, "" ): 37

position of even after s.replace(0, 4, "" ): 22
position of  don't after s.replace(0, 4, "" ): 33

position even found in complete expression: 22
position don't found in complete expression: 33

Result: I have heard it works only if you believe in it

結果Visual Studio(ライブで見る):

position of even before s.replace(0, 4, "" ): 26
position of  don't before s.replace(0, 4, "" ): 37

position of even after s.replace(0, 4, "" ): 22
position of  don't after s.replace(0, 4, "" ): 33

position  don't found in complete expression: 37
position even found in complete expression: 26
Result: I have heard it works evenonlyyou donieve in it

規格の詳細

部分式の評価が順序付けされていないことが指定されていない限り、これはドラフト C++11 標準セクション1.9 プログラムの実行からのものであることがわかっています。

特に断りのない限り、個々の演算子のオペランドおよび個々の式の部分式の評価は順不同です。[...]

そして、関数呼び出しは、関数本体に関して、関数呼び出し後置式と引数のシーケンス前の関係を導入することを知っています1.9

[...]関数を呼び出すとき (関数がインラインであるかどうかに関係なく)、すべての値の計算と引数式、または呼び出された関数を指定する後置式に関連付けられた副作用は、すべての式またはステートメントの実行前に順序付けられます。呼び出された関数の本体で。[...]

5.2.5 また、クラスメンバー アクセス、したがってチェーンが左から右に評価されることもわかっています。

[...]ドットまたは矢印の前の接尾式が評価されます。64 その評価の結果は、id-expression とともに、後置式全体の結果を決定します。

id-expressionが最終的に非静的メンバー関数になる場合、それは別のサブ式であるため、内の式リストの評価順序を指定しないことに注意してください。Postfix 式()からの関連する文法:5.2

postfix-expression:
    postfix-expression ( expression-listopt)       // function call
    postfix-expression . templateopt id-expression // Class member access, ends
                                                   // up as a postfix-expression

C++17 の変更点

提案p0145r3: Refining Expression Evaluation Order for Idiomatic C++は、いくつかの変更を加えました。postfix-expressionとそのexpression-listの評価規則の順序を強化することにより、コードに明確に指定された動作を与える変更を含めます。

[expr.call]p5言います:

postfix-expression は、expression-list およびすべてのデフォルト引数の各式の前に並べられます。関連するすべての値の計算と副作用を含むパラメーターの初期化は、他のパラメーターの初期化に対して不確定に順序付けられます。[ 注: 引数評価のすべての副作用は、関数に入る前に順序付けされます (4.6 を参照)。—終わりのメモ] [例:

void f() {
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, "");
assert(s == "I have heard it works only if you believe in it"); // OK
}

—終わりの例]

于 2014-11-26T21:02:34.740 に答える