4

私は最近、方程式ソルバーで奇妙な動作に出くわしました。これにより、移動セマンティクスと RVO がどのように連携するかを本当に理解しているかどうか自問するようになりました。

このフォーラムには関連する質問がたくさんあり、これに関する一般的な説明もたくさん読みました。しかし、私の問題は非常に具体的であるように思われるので、誰かが私を助けてくれることを願っています.

関連する構造体は全体的に少し複雑ですが、少なくとも次のように分類されます。

struct Foo
{
    Bar* Elements;

    Foo(void) : Elements(nullptr)
    {
        cout << "Default-constructing Foo object " << this << endl;
    }

    Foo(Foo const& src) : Elements(nullptr)
    {
        cout << "Copying Foo object " << &src << " to new object " << this << endl;
        if (src.Elements != nullptr)
        {
            Allocate();
            copy (src.Elements, src.Elements + SIZE, Elements);
        }
    }

    Foo(Foo&& src) : Elements(nullptr)
    {
        cout << "Moving Foo object " << &src << " into " << this << endl;
        Swap(src);
    }

    ~Foo(void)
    {
        cout << "Destructing Foo object " << this << endl;
        Deallocate();
    }

    void Swap(Foo& src)
    {
        cout << "Swapping Foo objects " << this << " and " << &src << endl;
        swap(Elements, src.Elements);
    }

    void Allocate(void)
    {
        Elements = new Bar[SIZE]();
    }

    void Deallocate(void)
    {
        delete[] Elements;
    }

    Foo& operator=(Foo rhs)
    {
        cout << "Assigning another Foo object to " << this << endl;
        Swap(rhs);
        return *this;
    }

    Foo& operator+=(Foo const& rhs)
    {
        cout << "Adding Foo object " << &rhs << " to " << this << endl;
        // Somehow adding rhs to *this
        cout << "Added Foo object" << endl;
        return *this;
    }

    Foo operator+(Foo rhs) const
    {
        cout << "Summing Foo objects" << endl;
        return rhs += *this;
    }

    static Foo Example(void)
    {
        Foo result;
        cout << "Creating Foo example object " << &result << endl;
        // Somehow creating an 'interesting' example
        return result;
    }
};

次の短いプログラムを考えてみましょう。

int main()
{
    Foo a = Foo::Example();
    cout << "Foo object 'a' is stored at " << &a << endl;
    Foo b = a + a;
    cout << "Foo object 'b' is stored at " << &b << endl;
}

これらは、このコードを実行する前の私の期待でした:

  1. このExampleメソッドはローカルFooオブジェクトをインスタンス化し、デフォルトの ctorが呼び出されます。
  2. ExampleFooローカルオブジェクトを値で返します。ただし、このコピーはRVOのために省略されると思います。
  3. コピー ctorへの後続の呼び出しも最適化される可能性があります。代わりaに、 内の一時オブジェクトのアドレスが与えられる場合がありますExample
  4. 式を評価するために、左側のオペランドでメソッドが呼び出されますa + aoperator+
  5. 右側のオペランドは値で渡されるため、ローカル コピーを作成する必要がある場合があります。
  6. メソッド内では、参照渡しoperator+=でそのコピーに対して呼び出されます。*this
  7. ここで、呼び出し元のメソッドoperator+=の return ステートメントにジャンプして、再び同じローカル コピーへの参照を返します。operator+
  8. 参照されたオブジェクトは、最終的に値によって返されます。ここでは、ローカル コピーの値を保持する必要があるだけなのでb(以前のステップ 2 と 3 で発生したように)、別のコピー省略が予想されます。
  9. オブジェクトaとオブジェクトの両方bが最終的にスコープ外になるため、それらのデストラクタを呼び出します。

(少なくとも私にとって) 驚くべき観察結果は、ステップ 8 でディープ コピーが最適化されていないことです (使用されたコンパイラ オプションに関係なく)。代わりに、出力は次のようになります。

01  Default-constructing Foo object 0x23fe20
02  Creating Foo example object 0x23fe20
03  Foo object 'a' is stored at 0x23fe20
04  Copying Foo object 0x23fe20 to new object 0x23fe40
05  Summing Foo objects
06  Adding Foo object 0x23fe20 to 0x23fe40
07  Added Foo object
08  Copying Foo object 0x23fe40 to new object 0x23fe30
09  Destructing Foo object 0x23fe40
10  Foo object 'b' is stored at 0x23fe30
11  Destructing Foo object 0x23fe30
12  Destructing Foo object 0x23fe20

の次の小さな変更は、operator+まったく違いがないように見えました。

Foo operator+(Foo rhs) const
{
    cout << "Summing Foo objects" << endl;
    rhs += *this;
    return rhs;
}

ただし、今回の結果は完全に異なります。

01  Default-constructing Foo object 0x23fe20
02  Creating Foo example object 0x23fe20
03  Foo object 'a' is stored at 0x23fe20
04  Copying Foo object 0x23fe20 to new object 0x23fe40
05  Summing Foo objects
06  Adding Foo object 0x23fe20 to 0x23fe40
07  Added Foo object
08  Moving Foo object 0x23fe40 into 0x23fe30
09  Swapping Foo objects 0x23fe30 and 0x23fe40
10  Destructing Foo object 0x23fe40
11  Foo object 'b' is stored at 0x23fe30
12  Destructing Foo object 0x23fe30
13  Destructing Foo object 0x23fe20

明らかに、コンパイラはxvaluerhsとして認識され(明示的に を記述した場合と同様に)、代わりにmove ctorを呼び出します。return move(rhs += *this);

さらに、-fno-elide-constructorsオプションを使用すると、常に次のようになります。

01  Default-constructing Foo object 0x23fd30
02  Creating Foo example object 0x23fd30
03  Moving Foo object 0x23fd30 into 0x23fe40
04  Swapping Foo objects 0x23fe40 and 0x23fd30
05  Destructing Foo object 0x23fd30
06  Moving Foo object 0x23fe40 into 0x23fe10
07  Swapping Foo objects 0x23fe10 and 0x23fe40
08  Destructing Foo object 0x23fe40
09  Foo object 'a' is stored at 0x23fe10
10  Copying Foo object 0x23fe10 to new object 0x23fe30
11  Summing Foo objects
12  Adding Foo object 0x23fe10 to 0x23fe30
13  Added Foo object
14  Moving Foo object 0x23fe30 into 0x23fe40
15  Swapping Foo objects 0x23fe40 and 0x23fe30
16  Moving Foo object 0x23fe40 into 0x23fe20
17  Swapping Foo objects 0x23fe20 and 0x23fe40
18  Destructing Foo object 0x23fe40
19  Destructing Foo object 0x23fe30
20  Foo object 'b' is stored at 0x23fe20
21  Destructing Foo object 0x23fe20
22  Destructing Foo object 0x23fe10

私が信じていることから、コンパイラは行かなければなりません

  1. RVO (可能であれば)、または
  2. 建設を移動する(可能であれば)、または
  3. コピー構築(そうでなければ)、

その順序で。だから私の質問は: 誰かが私に説明してもらえますか、ステップ 8 で実際に何が起こるのか、なぜ上記の優先順位の規則が適用されないのか (または、もしそうなら、ここに表示されていないものは何ですか)? 冗長な例で申し訳ありませんが、事前に感謝します。

-std=c++11現在、最適化をオフにしてgcc mingw-w64 x86-64 v.4.9.2を使用しています。

ps - 適切な OO コードの書き方とカプセル化の方法について私にアドバイスしたいという衝動を抑えてください ;-)

4

2 に答える 2

3

値渡しのパラメーターは NRVO の対象ではない (なぜ値渡しのパラメーターは NRVO から除外されるのですか? ) ため、代わりに移動されます (値によって返される場合、値パラメーターは暗黙的に移動されますか? )

かなり単純な解決策は、関数本体内で const 参照とコピーによって両方のパラメーターを取得することです。

Foo operator+(Foo const& rhs) const
{
    cout << "Summing Foo objects" << endl;
    Foo res{*this};
    res += rhs;
    return res;
}
于 2015-03-19T18:27:57.750 に答える
2

一時的なものを取り除きたい場合は、次の実装を使用することをお勧めします。

Foo operator+(const Foo& rhs) const
{
    cout << "Summing Foo objects" << endl;
    Foo result(rhs);
    result += *this;
    return result;
}

これにより、NRVO を適用できます。2 番目のバージョンは「十分にスマートなコンパイラ」によって最適化されている可能性がありますが、私のものは現在、ほとんどのコンパイラで動作します。これは実際には標準の問題ではなく、コンパイラの実装の品質の問題です。

また、ほとんどの定型コードを実装するBoost.Operatorsdf.operatorsなどのライブラリをチェックアウトすることもできます。

于 2015-03-19T18:16:00.090 に答える