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:
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. a
might be given the address of the temporary object inside Example
. a + a
, the operator+
method is called on the left-hand operand. operator+=
is called on that copy with *this
being passed by reference. operator+=
returns a reference to still the same local copy again, jumping back into the return statement of the calling operator+
method. b
now (as happened before in step 2 and 3). 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
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 ++编译错误?
下一篇: 为什么既不移动语义也不会按预期工作?