web-dev-qa-db-ja.com

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

Bjarne StroustrupのC++プログラミング言語第4版セクション36.3.6STL-like Operationschaining の例として、次のコードが使用されます。

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ライブで見る)が、 Clangライブで見る)。

なぜ異なる結果が得られるのですか?これらのコンパイラのいずれかがチェーン式を誤って評価しているか、このコードは nspecified または ndefined behavior の形式を示していますか?

94
Shafik Yaghmour

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

この例は、提案で言及されています N4228:Idiomatic C++の式評価順序の改良 質問のコードについて次のように述べています:

[...]このコードは、世界中のC++エキスパートによってレビューされ、公開されています(The C++ Programming Language、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

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

  • Aは、Bの前にシーケンスされるCの前にシーケンスされるDの前にシーケンスされる
  • 1から9は、以下にリストされているいくつかの例外を除き、他の部分式に関して不確定にシーケンスされています
    • 1から3は、Bの前にシーケンスされます
    • 4から6は、Cの前にシーケンスされます
    • 7から9は、Dの前にシーケンスされます

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

  • 4から9は、Bに関して不確定にシーケンスされます

Bに関する4および7の評価選択の潜在的な順序は、f2()を評価するときのclanggccの結果の違いを説明します。私のテストでは、clang47を評価する前にBを評価しますが、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とともに、後置式全体の結果を決定します。

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

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:慣用的なC++の式評価順序の改良 は、いくつかの変更を加えました。 postfix-expressionsおよびそれらの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
}

—例の終了]

104
Shafik Yaghmour

これは、C++ 17に関する問題に関する情報を追加することを目的としています。 C++17の提案( 慣用的C++リビジョン2の式評価順序の改良 )は、上記のコードが見本であると述べた問題に対処しました。

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

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

次のプログラムフラグメントを検討してください。

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++専門家によってレビューされ、公開されています(The C++ Programming Language、第4版)。しかし、その不特定の評価順序に対する脆弱性は、ツールによって最近発見されました。

この論文は、Cの影響を受け、30年以上存在していた式評価の順序に関するpre _C++17ルールの変更を提案しました。 言語は現代的なイディオムまたは「」「危険を発見するのが難しい、不明瞭なトラップとソース」などのリスクを保証すべきだと提案した上記のコード標本。

C++17の提案は、すべての式に明確に定義された評価順序が必要であること:です。

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

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

4
ricky m