コードは、未定義の動作を引き起こしませんが、副式の評価の順序が指定されていないため、指定されていない動作を示します。この場合、すべての副作用は、副作用の間にシーケンス関係を導入する関数内で行われるためです。
この例は、提案N4228:Idiomatic C ++の式評価順序の絞り込みで言及されており、質問のコードについて次のように述べています。
[...]このコードは、世界中のC ++専門家によってレビューされ、公開されています(C ++プログラミング言語、第 4 版)。しかし、不特定の評価順序に対する脆弱性は、最近ツールによって発見されました[.. 。]
細部
関数への引数が不特定の評価順序を持っていることは多くの人にとって明白かもしれませんが、この動作が連鎖関数呼び出しとどのように相互作用するかはおそらく明白ではありません。私が最初にすべてにこのケースを分析し、明らかでないとき、それは私には明らかではなかった専門家の査読のいずれか。
一見すると、それぞれreplace
を左から右に評価する必要があるため、対応する関数引数グループも左から右のグループとして評価する必要があるように見えます。
これは不正解です。関数の引数の評価順序は不特定ですが、関数呼び出しを連鎖すると、各関数呼び出しに左から右への評価順序が導入されますが、各関数呼び出しの引数は、その一部であるメンバー関数呼び出しに関してのみ順序付けられます。の。特に、これは次の呼び出しに影響します。
s.find( "even" )
そして:
s.find( " don't" )
これは、以下に関して不確定に順序付けられます。
s.replace(0, 4, "" )
2つのfind
呼び出しはの前または後に評価できます。replace
これは、s
の結果を変更するような副作用find
があり、の長さを変更するため重要ですs
。そのreplace
ため、2つのfind
呼び出しに対してそれがいつ評価されるかによって、結果は異なります。
連鎖式を見て、いくつかの部分式の評価順序を調べると、次のようになります。
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
注意してください、4
そして7
さらに多くの部分式に分解できるという事実を無視しています。そう:
A
前B
にシーケンスされる前にシーケンスされる前C
にシーケンスされるD
1
する9
不定下記の例外の一部と他の部分式に対して配列決定します
1
3
前にシーケンスされますB
4
6
前にシーケンスされますC
7
9
前にシーケンスされますD
この問題の鍵は次のとおりです。
評価の選択の潜在的な秩序4
と7
に関しては、B
との結果の違いを説明clang
し、gcc
評価をf2()
。私のテストでは、clang
評価のB
前に評価し4
、7
whileのgcc
後に評価します。次のテストプログラムを使用して、各ケースで何が起こっているかを示すことができます。
#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とともに、postfix式全体の結果を決定します。
id-expressionが最終的に非静的メンバー関数になる場合は、それが別個のサブ式であるため、式リストの評価順序を指定しないことに注意してください()
。5.2
Postfix式からの関連文法:
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:Idiomatic C ++の式評価順序の絞り込みにより、いくつかの変更が行われました。postfix-expressionsとそれらのexpression-listの評価ルールの順序を強化することにより、コードを適切に指定した動作を提供する変更を含みます。
[expr.call] p5さんのコメント:
postfix-expressionは、expression-listの各式とデフォルトの引数の前に順番に並べられます。関連するすべての値の計算と副作用を含むパラメーターの初期化は、他のパラメーターの初期化に対して不確定にシーケンスされます。[注:引数評価のすべての副作用は、関数に入る前に順序付けされます(4.6を参照)。—end note] [例:
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
}
—例を終了]
s.replace( s.replace( s.replace(0, 4, "" ).find( "even" ), 4, "only" ).find( " don't" ), 6, "" );