メソッドチェーンにおけるC ++実行順序


108

このプログラムの出力:

#include <iostream> 
class c1
{   
  public:
    c1& meth1(int* ar) {
      std::cout << "method 1" << std::endl;
      *ar = 1;
      return *this;
    }
    void meth2(int ar)
    {
      std::cout << "method 2:"<< ar << std::endl;
    }
};

int main()
{
  c1 c;
  int nu = 0;
  c.meth1(&nu).meth2(nu);
}

です:

method 1
method 2:0

開始nu時に1ではないのはなぜmeth2()ですか?


41
@MartinBonner:私は答えを知っているが、私はに「明白な」それを呼び出すことはありません任意の単語の意味と、それがあったとしても、それはドライブバイためにdownvoteまともな理由ではないでしょう。がっかり!
オービットのライトネスレース2016年

4
これは、引数を変更したときに得られるものです。引数を変更する関数は読みにくく、その影響は、次のプログラマーがコードで作業するのに予期せず、このような驚きにつながります。呼び出し元以外のパラメータは変更しないことを強くお勧めします。最初の結果に対して2番目のメソッドが呼び出されるため、インボカントの変更はここでは問題になりません。そのため、エフェクトがそれに順序付けられます。ただし、そうでない場合もあります。
Jan Hudec、2016年


@JanHudecこれが、関数型プログラミングが関数の純粋さを非常に重視する理由です。
Pharap、2016年

2
例として、スタックベースの呼び出し規約は、おそらくプッシュを好むだろうnu&nucそれから呼び出し、その順番でスタックに上meth1、その後呼び出し、スタックの上で、結果をプッシュし、meth2レジスタベースの呼び出し規約がしたいと思う一方で、負荷c&nuレジスタに、呼び出しmeth1、ロードnuレジスタには、次に呼び出しますmeth2
Neil、

回答:


66

評価順は不定ですので。

呼ばれる前にも評価さnumainているのを見ています。これは連鎖の問題です。しないことをお勧めします。0meth1

見栄えがよく、シンプルで、明確で、読みやすく、理解しやすいプログラムを作成するだけです。

int main()
{
  c1 c;
  int nu = 0;
  c.meth1(&nu);
  c.meth2(nu);
}

14
この問題を修正する、いくつかのケース評価順序明確にする提案がC ++ 17で実現する可能性があります
Revolver_Ocelot

7
メソッドチェーン好きですが(例えば<<、出力、およびコンストラクタにあまりにも多くの引数を持つ複雑なオブジェクトのための「オブジェクトビルダー」のために-それは、出力引数を本当にひどくミックスけど。
マーティン・ボナーはモニカ・サポート

34
この権利を理解できますか?評価の順序meth1meth2定義されていますが、パラメータのための評価のmeth2前に起こるかもしれませんがmeth1呼ばれていますか...?
ロディ

7
メソッドが適切であり、インボカントのみを変更する限り、メソッドチェーンは問題ありません(最初の結果で2番目のメソッドが呼び出されるため、効果が適切に順序付けられます)。
Jan Hudec

4
あなたがそれについて考えるとき、それは論理的です。それは次のように機能しますmeth2(meth1(c, &nu), nu)
BartekChom 2016年

29

評価の順序に関する標準草案のこの部分は関連があると思います:

1.9プログラムの実行

...

  1. 特に明記されていない限り、個々の演算子のオペランドおよび個々の式の部分式の評価は、シーケンスされていません。演算子のオペランドの値計算は、演算子の結果の値計算の前にシーケンスされます。スカラーオブジェクトの副作用が同じスカラーオブジェクトの別の副作用または同じスカラーオブジェクトの値を使用した値の計算に関連してシーケンスされておらず、それらが潜在的に同時に発生しない場合、動作は未定義です。

そしてまた:

5.2.2関数呼び出し

...

  1. [注:後置式と引数の評価は、すべて相互に順序付けされていません。引数評価のすべての副作用は、関数に入る前に順序付けされます—終わりの注意]

したがって、あなたの行c.meth1(&nu).meth2(nu);では、への最後の呼び出しの関数呼び出し演算子に関してoperatorで何が起こっているかを考えてくださいmeth2。そうすれば、後置式と引数の内訳が明確にわかりますnu

operator()(c.meth1(&nu).meth2, nu);

最後の関数呼び出しの後置式と引数評価(つまり、後置式c.meth1(&nu).meth2nu)は、上記の関数呼び出し規則に従って、相互に順序付けられていません。したがって、スカラーオブジェクトに対する後置式の計算の副作用は、関数呼び出しの前の引数評価に比べて順序付けされていません。上記のプログラム実行ルールでは、これは未定義の動作です。arnumeth2

言い換えれば、コンパイラーが呼び出しの後に呼び出しのnu引数を評価する必要はありません- 評価に影響を与える副作用がないと仮定することは自由です。meth2meth1meth1nu

上記で生成されたアセンブリコードには、main関数に次のシーケンスが含まれています。

  1. 変数nuはスタックに割り当てられ、0で初期化されます。
  2. レジスター(ebx私の場合)は、値のコピーを受け取りますnu
  3. のアドレスnucパラメータレジスタへのロード
  4. meth1 呼ばれる
  5. 戻り値レジスタと、以前にキャッシュされた値nuebxレジスタは、パラメータ・レジスタにロードされています
  6. meth2 呼ばれる

重要なのは、上記のステップ5で、コンパイラーがnuステップ2のキャッシュ値をへの関数呼び出しで再利用できるようにすることmeth2です。ここでは、動作中の「未定義の動作」のnu呼び出しによって変更された可能性を無視meth1します。

注:この回答は、元の形式から実質的に変更されています。最後の関数呼び出しの前にシーケンスされていないオペランド計算の副作用に関する私の最初の説明は正しくありませんでした。問題は、オペランド自体の計算が不確定に順序付けられるという事実です。


2
これは間違っています。関数の呼び出しは、呼び出し元の関数で他の評価を使用して不確定に順序付けされます(前に順序付けされた制約が課されていない限り)。それらはインターリーブしません。
TC

1
@TC-インターリーブされる関数呼び出しについては何も言わなかった。演算子の副作用についてのみ言及しました。上記で生成されたアセンブリコードを見ると、がmeth1before meth2に実行されていることがわかりますが、のパラメーターmeth2nu、呼び出しの前にレジスターにキャッシュされた値です。meth1つまり、コンパイラーは潜在的な副作用を無視しました。私の答えと一致しています。
Smeeheey

1
「その副作用(つまり、arの値の設定)は、呼び出しの前にシーケンスされることが保証されていない」と、あなたは正確に主張しています。関数呼び出しの後置発現の評価(であるc.meth1(&nu).meth2)、そのコール(引数の評価nu)一般unsequencedされているが、1)その副作用は、すべてに入る前に配列決定されているmeth2ので2)c.meth1(&nu)関数呼び出しがあります、それはの評価で不確定に順序付けられますnu。インサイドmeth2それは何らかの形での変数へのポインタを取得した場合、mainそれは常に1見るでしょう
TC

2
「しかし、オペランドの計算の副作用(つまり、arの値を設定すること)は、(上記の2のように)何よりも前に順序付けられることが保証されていません。」meth2引用しているcppreferenceページの項目3に記載されているように(正しく引用することも怠っています)、への呼び出しの前に順序付けられることが絶対に保証されています。
TC

1
あなたは何かを間違って、それをさらに悪化させました。ここでは未定義の動作は絶対にありません。例を過ぎて、[intro.execution] / 15を読み続けてください。
TC

9

1998 C ++標準、セクション5、4項

特に明記されていない限り、個々の演算子のオペランドと個々の式の部分式の評価の順序、および副作用が発生する順序は指定されていません。前のシーケンスポイントと次のシーケンスポイントの間で、スカラーオブジェクトの保存された値は、式の評価によって最大で1回変更されます。さらに、以前の値は、保存される値を決定するためにのみアクセスされるものとします。この段落の要件は、完全な式の部分式の許容される順序ごとに満たされるものとします。それ以外の場合の動作は未定義です。

(この質問に関係のない脚注#53への参照は省略しています)。

基本的に、を&nu呼び出す前に評価する必要がありc1::meth1()、を呼び出す前に評価するnu必要がありますc1::meth2()。ただし、nu以前に評価する必要のある要件はありません&nu(たとえばnu、最初に評価され、次に&nu、次にc1::meth1()呼び出されることが許可されます-これはコンパイラが行っていることかもしれません)。したがって、式*ar = 1in c1::meth1()は、nuin main()が評価される前に評価され、に渡されることが保証されていませんc1::meth2()

後のC ++標準(今夜使用しているPCには現在ありません)には、基本的に同じ句があります。


7

コンパイルするとき、関数meth1とmeth2が実際に呼び出される前に、パラメーターがそれらに渡されていると思います。「c.meth1(&nu).meth2(nu);」を使用する場合を意味します 値nu = 0がmeth2に渡されているので、「nu」が後で変更されてもかまいません。

あなたはこれを試すことができます:

#include <iostream> 
class c1
{
public:
    c1& meth1(int* ar) {
        std::cout << "method 1" << std::endl;
        *ar = 1;
        return *this;
    }
    void meth2(int* ar)
    {
        std::cout << "method 2:" << *ar << std::endl;
    }
};

int main()
{
    c1 c;
    int nu = 0;
    c.meth1(&nu).meth2(&nu);
    getchar();
}

それはあなたが望む答えを得るでしょう

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