Archive
The Move That Wasn’t
While trying to duplicate the results I measured in my “Time To Get Moving!” post, an astute viewer posted this comment:
For your convenience, I re-list the code in question here for your inspection:
#include <iostream> #include <vector> #include <utility> #include <chrono> using namespace std; struct Msg{ vector<double> vDoubs; Msg(const int NUM_ELS) : vDoubs(NUM_ELS){ } }; int main() { //Construct a big Msg object! const int NUM_ELS = 10000000; Msg msg1{NUM_ELS}; //reduce subsequent code verbosity using std::chrono::steady_clock; using std::chrono::duration_cast; using std::chrono::microseconds; //Measure the performance of //the "free" copy operations auto tStart = steady_clock::now(); Msg msg2{msg1}; //copy ctor msg1 = msg2; //copy assignment auto tElapsed = steady_clock::now() - tStart; cout << "Copy ops took " << duration_cast<microseconds>(tElapsed).count() << " microseconds\n"; //Measure the performance of //the "free" move operations tStart = steady_clock::now(); Msg msg3{std::move(msg1)}; //move ctor msg1 = std::move(msg3); //move assignment tElapsed = steady_clock::now() - tStart; cout << "Move ops took " << duration_cast<microseconds>(tElapsed).count() << " microseconds\n"; cout << "Size of moved-from object = " << msg3.vDoubs.size(); } //"free" dtor is executed here for msg1, msg2, msg3
Sure enough, I duplicated what my friend Gyula discovered about the “move” behavior of the VS2013 C++ compiler:
Intrigued by the finding, I dove deeper into the anomaly and dug up these statements in the fourth edition of Bjarne Stroustrup’s “The C++ Programming Language“:
Since the Msg structure in the code listing does not have any copy or move operations manually defined within its definition, the compiler is required to generate them by default. However, since Bjarne doesn’t state that a compiler is required to execute moves when the programmer explicitly directs it to with std::move() expressions, maybe the compiler isn’t required to do so. However, common sense dictates that it should. Hopefully, the next update to the VS2013 compiler will do the right thing – like GCC (and Clang?) currently does.
Time To Get Moving!
Prior to C++11, for every user-defined type we wrote in C++03, all standards-conforming C++ compilers gave us:
- a “free” copy constructor
- a “free” copy assignment operator
- a “free” default constructor
- a “free” destructor
The caveat is that we only got them for free if we didn’t manually override the compiler and write them ourselves. And unless we defined reference or pointer members inside of our type, we didn’t have to manually write them.
Starting from C++11 on, we not only get those operations for free for our user-defined types, we also get these turbo-boosters:
- a “free” move constructor
- a “free” move assignment operator
In addition, all of the C++ standard library containers have been “move enabled“.
When I first learned how move semantics worked and why this new core language feature dramatically improved program performance over copying, I started wondering about user-defined types that wrapped move-enabled, standard library types. For example, check out this simple user-defined Msg structure that encapsulates a move-enabled std::vector.
Logic would dictate that since I get “move” operations from the compiler for free with the Msg type as written, if I manually “moved” a Msg object in some application code, the compiler would “move” the vDoubs member under the covers along with it – for free.
Until now, I didn’t test out that deduction because I heard my bruh Herb Sutter say in a video talk that deep moves came for free with user-defined types as long as each class member in the hierarchical composition is also move-enabled. However, in a more recent video, I saw an excellent C++ teacher explicitly write a move constructor for a class similar to the Msg struct above:
D’oh! So now I was confused – and determined to figure out was was going on. Here is the program that I wrote to not only verify that manually written “move” operations are not required for the Msg struct, but to also measure the performance difference between moving and copying:
First, the program built cleanly as expected because the compiler provided the free “move” operations for the Msg struct. Second, the following, 5-run, output results proved that the compiler did indeed perform the deep, under the covers, “move” that my man Herb promised it would do. If the deep move wasn’t executed, there would have been no noticeable difference in performance between the move and copy operations.
From the eye-popping performance difference shown in the results, we should conclude that it’s time to start replacing copy operations in our code with “move” operations wherever it makes sense. The only thing to watch out for when moving objects from one place to another is that in the scope of the code that performs the move, the internal state of the moved-from object is not needed or used by the code following the move. The following code snippet, which prints out 0, highlights this behavior.
Looks Weird, But Don’t Be Afraid To Do It
Modern C++ (C++11 and onward) eliminates many temporaries outright by supporting move semantics, which allows transferring the innards of one object directly to another object without actually performing a deep copy at all. Even better, move semantics are turned on automatically in common cases like pass-by-value and return-by-value, without the code having to do anything special at all. This gives you the convenience and code clarity of using value types, with the performance of reference types. – C++ FAQ
Before C++11, you’d have to be tripping on acid to allow a C++03 function definition like this to survive a code review:
std::vector<int> moveBigThingsOut() { const int BIGNUM(1000000); std::vector<int> vints; for(int i=0; i<BIGNUM; ++i) { vints.push_back(i); } return vints; }
Because of: 1) the bolded words in the FAQ at the top of the page, 2) the fact that all C++11 STL containers are move-enabled (each has a move ctor and a move assignment member function implementation), 3) the fact that the compiler knows it is required to destruct the local vints object just before the return statement is executed, don’t be afraid to write code like that in C++11. Actually, please do so. It will stun reviewers who don’t yet know C++11 and may trigger some fun drama while you try to explain how it works. However, unless you move-enable them, don’t you dare write code like that for your own handle classes. Stick with the old C++03 pass by reference idiom:
void passBigThingsOut(std::vector<int>& vints) { const int BIGNUM(1000000); vints.clear(); for(int i=0; i<BIGNUM; ++i) { vints.push_back(i); } }
Which C++11 user code below do you think is cleaner?
//This one-liner? auto vints = moveBigThingsOut(); //Or this two liner? std::vector<int> vints{}; passBigThingsOut(vints);