Why do neither move semantics nor RVO work as expected?

I have recently stumbled upon some strange behaviour in my equation solver, which made me ask myself if I really understood how move semantics and RVO work together.

There are plenty of related questions on this forum, and I've also read many general explanations on this. But my problem seems to be quite specific so I hope someone will help me out.

The involved struct is a bit complex altogether but it breaks at least down to this:

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

Now let's consider the following short program:

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

These were my expectations before I ran this code:

  • The Example method instantiates a local Foo object, resulting in the default ctor being called.
  • Example returns the local Foo object by value. However, I'd expect this copy to be elided due to RVO.
  • The subsequent call to the copy ctor may also get optimized out. Instead a might be given the address of the temporary object inside Example .
  • In order to evaluate the expression a + a , the operator+ method is called on the left-hand operand.
  • The right-hand operand is passed by value, so a local copy might have to be made.
  • Inside the method, operator+= is called on that copy with *this being passed by reference.
  • Now the operator+= returns a reference to still the same local copy again, jumping back into the return statement of the calling operator+ method.
  • The referenced object is finally returned by value. Here I'd anticipate another copy elision, since the value of the local copy merely has to be held by b now (as happened before in step 2 and 3).
  • Both objects a and b will eventually be going out of scope hence calling their destructors.
  • The surpring observation (at least for me) is, that in step 8 the deep copy is not optimized out (no matter what compiler options used). Instead, the output looks like this:

    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
    

    The following small change in the operator+ appeared to me to make no difference at all:

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

    However the outcome is completly different this time:

    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
    

    Obviously the compiler recognized rhs as an xvalue now (like it also does if I explicitly write return move(rhs += *this); ) and calls the move ctor instead.

    Besides, with the -fno-elide-constructors option you'll always get this:

    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
    

    From what I believe, the compiler has to go for

  • RVO (if possible), or
  • Move construction (if possible), or
  • Copy construction (otherwise),
  • in that order. So my question is: Can someone please explain to me, what really happens in step 8 and why the above rule of precedence does not apply (or if so, what it is that I don't see here)? Sorry for the verbose example and thanks in advance.

    I am currently using gcc mingw-w64 x86-64 v.4.9.2 with -std=c++11 and optimizations off.

    ps - please resist the urge to advise me on how to write proper OO code and ensure encapsulation ;-)


    By-value parameters aren't subject to NRVO (Why are by-value parameters excluded from NRVO?) so they are moved instead (Are value parameters implicitly moved when returned by value?)

    A fairly simple solution is to take both parameters by const reference and copy within the function body:

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

    If you want to get rid of temporaries, I suggest you use the following implementation:

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

    which allows the NRVO to be applied. Your second version might be optimized away by a "Sufficiently Smart Compiler", but mine works today on most compilers. It's not really an issue with the standard, but with the quality of implementation of compilers.

    You could also check out libraries like Boost.Operators or df.operators which will implement most of the boiler-plate code for you.

    链接地址: http://www.djcxy.com/p/86818.html

    上一篇: C ++编译错误?

    下一篇: 为什么既不移动语义也不会按预期工作?