1. ホーム
  2. c++

[解決済み] The C++ Programming Language" 4th edition section 36.3.6 にあるこのコードは、動作がきちんと定義されているか?

2023-02-22 12:05:16

質問

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 ( ライブを見る を使用した場合は失敗しませんが Clang ( ライブを見る ).

なぜ異なる結果が得られるのでしょうか。これらのコンパイラーのいずれかが連鎖式の評価を誤っているのか、このコードが何らかの形で 未定義 または 未定義の動作 ?

どのように解決するのですか?

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

この例は、提案の中で言及されている N4228: イディオマティック C++ のための式の評価順序の改良 で、質問のコードについて次のように書かれています。

[...]このコードは、世界中のC++の専門家によってレビューされ、出版されています。 (The C++ Programming Language, 4 th 版)。しかし,評価順序の不定性に対する脆弱性 しかし、評価順序が指定されていないことに対する脆弱性は、つい最近、ツール[...]によって発見されました。 ツールによって発見されました[...]。

詳細

関数への引数が不特定の評価順序を持つことは多くの人にとって明白かもしれませんが、この動作が連鎖した関数呼び出しとどのように相互作用するかは、おそらくそれほど明白ではないでしょう。私が最初にこのケースを分析したとき、それは私には明らかではありませんでしたし、明らかにすべての 専門家のレビュアー も同様です。

一見したところ、それぞれの 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

この問題のキーポイントは

  • 49 に関しては、不定期に配列されます。 B

の評価選択の可能性のある順序は 4 そして 7 に対して B との結果の違いを説明しています。 clanggcc を評価するとき f2() . 私のテストでは clangB を評価する前に 47 一方 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 プログラムの実行 というのがあります。

特記されている場合を除き、個々の演算子のオペランドの評価と個々の式の部分式の評価は および個々の式の部分式の評価は非シーケンスである[...]。

で、関数呼び出しは関数本体に対して関数呼び出しの後置表現と引数のsequenced beforeの関係を導入することが分かっており、セクション 1.9 :

[...]関数を呼び出すとき(関数がインラインであるかどうかにかかわらず)、すべての 引数式に関連するすべての値の計算と副作用が発生します。 式、または呼び出された関数を指定する接尾辞式に関連するすべての値の計算と副作用は、すべての式の実行前にシーケンスされます。 関数を呼び出す場合(インラインであるかどうかに関わらず)、引数式、または呼び出された関数を指定する後置式に関連するすべての値の計算と副作用は、呼び出された関数の本体内のすべての式または文の実行前にシーケンスされます。 ステートメントの実行前にシーケンスされます。

また、クラスメンバーアクセスとその連鎖は左から右へ評価されることが分かっており、セクション 5.2.5 クラスメンバーアクセス というのがあります。

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

注意点として id-式 が非静的メンバ関数である場合、その評価順序は指定されません。 式リスト の中で () の中にある式リストは別の部分式であるためです。の関連する文法は 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: 慣用的な C++ のための式の評価順序の改良 は、いくつかの変更を行いました。の評価ルールの順序を強化することで、コードによく指定された動作を与えるような変更が含まれています。 後置表現 とその 式リスト .

[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
}

-終了例 ]。