4

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:

  1. The Example method instantiates a local Foo object, resulting in the default ctor being called.
  2. Example returns the local Foo object by value. However, I'd expect this copy to be elided due to RVO.
  3. 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.
  4. In order to evaluate the expression a + a, the operator+ method is called on the left-hand operand.
  5. The right-hand operand is passed by value, so a local copy might have to be made.
  6. Inside the method, operator+= is called on that copy with *this being passed by reference.
  7. Now the operator+= returns a reference to still the same local copy again, jumping back into the return statement of the calling operator+ method.
  8. 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).
  9. 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

  1. RVO (if possible), or
  2. Move construction (if possible), or
  3. 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.

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

Rene R.
  • 534
  • 1
  • 4
  • 12

2 Answers2

3

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;
}
Community
  • 1
  • 1
ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • 1
    Isn't this exactly what's under the OP's "The following small change in the operator+ appeared to me to make no difference at all:"? Or am I overlooking a change in your answer? (At any rate, your answer gives the explanation the OP is looking for.) –  Mar 19 '15 at 18:30
  • @hvd It does not answer the question as OP expected RVO instead of move for that case. – Daniel Frey Mar 19 '15 at 18:32
  • @DanielFrey Oh, yeah, you're right. In general, RVO doesn't and cannot work to construct a function parameter directly in the return area, but neither this nor your answer explains why. –  Mar 19 '15 at 18:35
  • Oh, yeah. Anyway this is probably http://stackoverflow.com/questions/6009004/are-value-parameters-implicitly-moved-when-returned-by-value?lq=1 – ecatmur Mar 19 '15 at 18:36
  • Or actually http://stackoverflow.com/questions/6009278/why-are-by-value-parameters-excluded-from-nrvo?lq=1 – ecatmur Mar 19 '15 at 18:37
  • "By-value parameters aren't subject to NRVO so they are moved instead" I see, but why is there another copy in the first version instead of a move? – Rene R. Mar 21 '15 at 19:47
  • @Rene R. In the first version the compiler has to copy because it doesn't know that what you're returning is a local or parameter - there's no requirement on language level that operator+= returns a reference to *this. – ecatmur Mar 21 '15 at 20:04
2

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.

Daniel Frey
  • 55,810
  • 13
  • 122
  • 180