「C ++プログラミング言語」第4版のセクション36.3.6のこのコードには、明確に定義された動作がありますか?


94

Bjarne StroustrupのC ++プログラミング言語の第4版セクションの36.3.6 STLに似た操作では、次のコードがチェーンの例として使用されています

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

でアサートが失敗したgccそれが生き参照)とVisual Studioそれが生きる参照)、それが使用しているときに失敗しないクランをそれが生きる参照します)。

なぜ結果が異なるのですか?これらのコンパイラーのいずれかがチェーン式を誤って評価していますか、それともこのコードは何らかの形の未指定または未定義の動作を示していますか?


より良い:s.replace( s.replace( s.replace(0, 4, "" ).find( "even" ), 4, "only" ).find( " don't" ), 6, "" );
Ben Voigt 2014年

20
バグはさておき、このような醜いコードを本に含めるべきではないと私が思うのは私だけですか?
Karoly Horvath 2014年

5
@KarolyHorvath注意cout << a << b << c≡はoperator<<(operator<<(operator<<(cout, a), b), c)わずかに少ない醜いです。
Oktalist 2014

1
@Oktalist::)少なくとも私はそこに意図を持っています。引数に依存する名前のルックアップと演算子の構文を簡潔な形式で同時に教えます。実際にそのようなコードを書くべきだという印象を与えません。
Karoly Horvath、2014

回答:


104

コードは、未定義の動作を引き起こしませんが、副式の評価の順序が指定されていないため、指定されていない動作を示します。この場合、すべての副作用は、副作用の間にシーケンス関係を導入する関数内で行われるためです。

この例は、提案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さらに多くの部分式に分解できるという事実を無視しています。そう:

  • ABにシーケンスされる前にシーケンスされる前CにシーケンスされるD
  • 1する9不定下記の例外の一部と他の部分式に対して配列決定します
    • 13前にシーケンスされますB
    • 46前にシーケンスされますC
    • 79前にシーケンスされますD

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

  • 4する9不定に関して配列決定されますB

評価の選択の潜在的な秩序47に関しては、Bとの結果の違いを説明clangし、gcc評価をf2()。私のテストでは、clang評価のB前に評価し47whileの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
}

—例を終了]


7
「多くの専門家」が問題を見落としていたことに少し驚いています。関数呼び出しのpostfix-expressionを評価することは、(すべてのバージョンのCおよびC ++で)引数を評価する前にシーケンスされないことがよく知られています。
MM

@ShafikYaghmour関数呼び出しは、相互に、およびその他すべてに関して、不確定に順序付けられます。ただし、前に順序付けられた関係は例外です。ただし、1、2、3、5、6、8、9 "even""don't"およびのいくつかのインスタンスの評価sは、相互に順序付けられていません。
2014年

4
@TCいいえ、そうではありません(この「バグ」が発生する方法です)。たとえばfoo().func( bar() )、呼び出しのfoo()前または後のいずれかで呼び出すことができますbar()後置式ですfoo().func。引数とpostfix-expressionは、の本体の前にfunc()順序付けられますが、互いに対して順序付けされていません。
MM、

@MattMcNabbああ、そうです、私は読み違いました。呼び出しではなく、postfix-expression自体について話しています。はい、そうです、もちろんシーケンスされていません(もちろん他のルールが適用されない限り)。
2014年

6
また、B.Stroustrupの本に登場するコードが正しいと考える傾向がある要因もあり、そうでなければ誰かがすでに気づいているはずです。(関連; SOユーザーはまだK&Rで新しい間違いを見つけます)
MM

4

これは、C ++ 17に関する問題に関する情報を追加することを目的としています。上記のコードを引用して問題に対処するための提案(Idiomatic C ++ Revision 2の式評価順序の洗練化C++17は、見本でした。

提案されたように、私は提案から関連情報を追加し、引用します(私のハイライト):

現在標準で指定されている式評価の順序は、アドバイス、一般的なプログラミングイディオム、または標準ライブラリ機能の相対的な安全性を損ないます。罠は初心者や不注意なプログラマのためだけのものではありません。彼らは私たちがルールを知っていても、無差別に私たち全員に影響を与えます。

次のプログラムの断片を考えてみましょう:

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

アサーションは、プログラマーの意図した結果を検証することになっています。メンバー関数呼び出しの「チェーン」を使用しますが、これは一般的な標準的な方法です。このコードは、世界的なC ++の専門家によってレビュー、および(言語、第4版のプログラミングC ++。)しかし、その公表された評価の不特定の順序に脆弱性がツールによって発見されたごく最近。

この論文は、30年以上C++17影響を受けC、存在してきた表現評価の順序に関する前ルールを変更することを提案しました。この言語は上記のコード例で何が起こったかなど、現代のイディオムまたはリスク「バグの発見の難しいトラップとソース」を保証するべきあると提案しまし

の提案でC++17、すべての式に明確に定義された評価順序が必要です

  • Postfix式は左から右に評価されます。これには、関数呼び出しとメンバー選択式が含まれます。
  • 割り当て式は右から左に評価されます。これには、複合割り当てが含まれます。
  • シフト演算子のオペランドは、左から右に評価されます。
  • オーバーロードされた演算子を含む式の評価の順序は、関数呼び出しの規則ではなく、対応する組み込み演算子に関連付けられた順序によって決まります。

上記のコードはGCC 7.1.1およびを使用して正常にコンパイルされますClang 4.0.0

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