N.B. there are mistakes in the code below which I discuss in the next post. The reason they exist is that this site is more like a journal and so is fairly raw in content since its intended audience is me.
I have been trying to understand how the new features of C++11 are going to affect the code that I write. I have read anecdotal evidence that the new move/rvalue functionality would help in writing more natural code.
This Article describes the new rvalue references in C++11.
The following is a program that I wrote to understand how the objects are copied through the function calls. It is a simplified example of how some production code I am working will behave. The following is a non POD class that simulates the data object that will exist in the real system.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
|
class Bar { //Non POD class int m1_; std::string m2_; static unsigned short id; public: Bar() { std::cout << "default" << std::endl; } ~Bar() { std::cout << "dtor(" << m1_ << ")" << std::endl; } Bar( const char* msg ) : m1_(id++),m2_(msg) { std::cout << "const char*(" << m1_ << ")" << std::endl; } Bar( const Bar& b ) : m1_(id++),m2_(b.m2_) { std::cout << "copy(" << m1_ << ")" << std::endl; } Bar& operator=( const Bar& b ) { std::cout << "assign(" << b.m1_ << ")" << std::endl; if( this != &b ) { Bar temp(b); std::swap(*this,temp); } return *this; } }; |
The code that follows uses the
class Bar . Basically at the heart there is a
queue<Bar> which contains instances of the
class Bar . This is populated indirectly via 2 layers of function calls. I began using a naive approach of passing by value because it is the simplest implementation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
class Foo { std::queue<Bar> barQueue; public: void FooFunc1(Bar b ) {std::cout << "FooFunc1" << std::endl;barQueue.push(b);} Bar FooFunc2() { std::cout << "FooFunc2" << std::endl; Bar out = barQueue.front(); barQueue.pop(); return out; } }; class WrappedFoo : Foo { public: void WFooFunc1(Bar b ) { std::cout << "WFooFunc1" << std::endl;Foo::FooFunc1(b); } Bar WFooFunc2() { std::cout << "WFooFunc2" << std::endl; return Foo::FooFunc2(); } }; |
The above is called by the following main function
|
|
int main(unsigned int argc, const char* argv[]) { Bar testObject("in"); WrappedFoo w; std::cout << "----1----" << std::endl; w.WFooFunc1(testObject); std::cout << "----2----" << std::endl; Bar t = w.WFooFunc2(); std::cout << "----3----" << std::endl; return 0; } |
The output with the pass by value approach is below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
const char*(0) ----1---- copy(1) WFooFunc1 copy(2) FooFunc1 copy(3) dtor(2) dtor(1) ----2---- WFooFunc2 FooFunc2 copy(4) dtor(3) copy(5) dtor(4) ----3---- dtor(5) dtor(0) |
In all the original object is copied 5 times through the process. The first optimisation I would do is to pass by reference through to the push into the queue. This will remove copies (1) & (2) above but there will be a copy made when the object is pushed. So now we are moving objects into the queue fairly efficiently. The return route still makes 2 copies before the return.
|
|
const char*(0) ----1---- WFooFunc1 FooFunc1 copy(1) ----2---- WFooFunc2 FooFunc2 copy(2) dtor(1) copy(3) dtor(2) ----3---- dtor(3) dtor(0) |
Bar t = w.WFooFunc2(); in the main function should on the face of it should make another copy, however it doesn’t because of Return Value Optimisation (RVO) or elision. This process means that the compiler will omit a copy of the object when returning from a function by value. It may not be possible if the function has multiple return points, or can return one of several objects.
So at this point I am looking at introducing the new move features of c++11.
|
|
Bar( const Bar&& b ) : m1_(b.m1_),m2_(b.m2_) { std::cout << "move(" << m1_ << ")" << std::endl; } Bar& operator=( Bar&& b ) { std::cout << "move assign(" << b.m1_ << ")" << std::endl; if( this != &b ) { std::swap(*this,b); } return *this; } |
The Bar object is modified to include a move constructor – much like a copy constructor, and move assignment operator. These are implemented in a similar way to the copy and assignment operator but are more efficient as they do not construct the object fully, they move the contents of the source into the new object leaving the old object as a shell – easily destroyed.
|
|
const char*( ----1---- WFooFunc1 FooFunc1 copy(1) ----2---- WFooFunc2 FooFunc2 copy(2) dtor(1) move(2) dtor(2) ----3---- dtor(2) dtor(0) |
Now we can see that simply adding these operators (actually just the move constructor) has made a further saving over the last version without any change in the code at all. The Temporary created in the return before is now a move operation.
The second copy above is made when the local is copied from the return value of
front() . The value is copied because a reference is returned and so the compiler cannot know what the expected lifetime of that object is. It cannot see that the next operation (the
pop() ) will cause it to be destoyed.
We can make a further change which brings into play a new function from the standard lib which forces the comiler to do the move:
|
|
Bar out = std::move(barQueue.front()); //move |
This then leaves us with :
|
|
const char*(0) ----1---- WFooFunc1 FooFunc1 copy(1) ----2---- WFooFunc2 FooFunc2 move(1) dtor(1) move(1) dtor(1) ----3---- dtor(1) dtor(0) |
Now we only make the copy when the object is pushed into the queue. So can I get rid of that as well? The answer is yes by adding a further call to
std::move() when I push the object passed by reference
|
|
const char*(0) ----1---- WFooFunc1 FooFunc1 move(0) ----2---- WFooFunc2 FooFunc2 move(0) dtor(0) move(0) dtor(0) ----3---- dtor(0) dtor(0) |
It turns out that I have made a slight mistake in this code that could have a bad effect in production code. I had instinctively realised but couldn’t place what the issue was. I started to talk about this in the next article and thinking about the situation made it clear what the issue is.