I've seen a recurring mistake made by well-versed C++03 programmers when they set out to use rvalue references for the first time. In fact, as it turns out, better you are at C++03, easier it is to fall in the trap of rvalue reference anti-pattern I'm gonna talk about.
Consider the following C++03 class:
The Book class above is as dull as it can be. Now lets C++11'fy it! C++11 gives us shiny new rvalue references and std::move. So lets add them.
Is our constructor optimally move-enabled? It's far from it! More often that not, programmers' legit code will end up calling the old constructor instead of the the new one, which probably means lost opportunities for optimization. It could be hard to see what's wrong in the new class to an untrained eye. We've test it to see what's really wrong with it.
It may come as a surprise that in all but one case above, the old constructor is called. Only in the last case, the new-ctor is called, which makes the minimum number of copies. So what's the gotcha?
As it turns out, the Book constructors are stopping the compiler from doing a better job. The first constructor takes all the parameters as const reference and the second one takes all the parameters as rvalue reference. Unless and until all the the parameters passed to the constructor are temporary objects (rvalues) or literals, the second constructor does not kick in. This implies lost optimization opportunities for parameters that are temporary (so can be moved) but won't be moved because some other parameter botched the soup. These two constructors give you very limited options: all-or-nothing.
In case of b1, authors is an lvalue and it does not bind to an rvalue ref.
In case of b2, year is an lvalue and does not bind to an rvalue ref.
In case of b3, toUpper returns a string reference; Does no good.
In case of b4, January returns a const string reference; same story!
In case of b5, year is an lvalue that prevents calling the second constructor although programmer tries to explicitly move all the string and vector parameters. In reality, the actual moves do not happen and if the remaining program depends on the moves being successful may not be very happy.
Only in case of b6, all actual parameters are rvalues or literals. Therefore, the "all-rvalue-ref" constructor is called. Note that temporary string objects will be implicitly created where string literals are used.
So lets fix it. What we really need is just one constructor that accepts all the parameters by value. As a side-effect, the overall code is much simpler.
That's it! This is our the constructor that works optimally. All strings, vectors, integral types are passed by value. Within the constructor, the pass-by-value parameters that are on the stack-frame must be moved to the respective data members. The reason is that the lifetime of pass-by-value parameters is anyways limited to the lifetime of the constructor call itself. We want to tie their lifetime to the object and not the constructor.
This constructor does no more deep copies of than necessary. And that number really depends on how it is called. Due to pass-by-value semantics, each object individually is an opportunity for the compiler to perform a move when a temporary is involved. Lets revisit our b1 to b4 objects.
In case of b1, except for authors all other parameters are copied and moved once.
In case of b2, all strings and vectors are copied and moved once. That's what matters.
In case of b3, toUpper returns an string reference; everything else copied and moved only once.
In case of b4, January returns a const string reference; everything else copied and moved once.
In case of b5, strings are vector are moved as expected and in fact the b5 object the local parameters are created using move-constructor and moved again into the data member. Hence the object is created without any deep copies (like zero-copy).
Using pass-by-value also opens other opportunities to eliminate temporaries when functions return by value and are used as actual parameters. However, I won't discuss that in detail here.
Finally, I want to conclude saying that this whole thing assumes that a move is much cheaper than a deep-copy. This is generally true for std::vector and std::string where memory is allocated dynamically. For small strings however small-string-optimization may make copy and move practically equivalent.
Consider the following C++03 class:
class Book { public: Book(const std::string & title, const std::vector<std::string> & authors, const std::string & pub, size_t pub_day const std::string & pub_month, size_t pub_year) : _title(title), _authors(authors), _publisher(pub), _pub_day(pub_day), _pub_month(pub_month), _pub_year(pub_year) {} // .... // .... private: std::string _title; std::vector<std::string> _authors; std::string _publisher; size_t _pub_day; std::string _pub_month; size_t _pub_year; };
The Book class above is as dull as it can be. Now lets C++11'fy it! C++11 gives us shiny new rvalue references and std::move. So lets add them.
class Book { public: Book(const std::string & title, const std::vector<std::string> & authors, size_t pub_day const std::string & pub_month, size_t pub_year) : _title (title), _author (author), _pub_day (pub_day), _pub_month(pub_month), _pub_year (pub_year) {} Book(std::string && title, std::vector<std::string> && authors, size_t && pub_day std::string && pub_month, size_t && pub_year) : _title (std::move(title)), _authors (std::move(authors)), _pub_day (pub_day), _pub_month(std::move(pub_month)), _pub_year (pub_year) {} // .... // .... private: std::string _title; std::vector<std::string> _authors; size_t _pub_day; std::string _pub_month; size_t _pub_year; };
Is our constructor optimally move-enabled? It's far from it! More often that not, programmers' legit code will end up calling the old constructor instead of the the new one, which probably means lost opportunities for optimization. It could be hard to see what's wrong in the new class to an untrained eye. We've test it to see what's really wrong with it.
std::string & toUpper(std::string & s) { std::transform(s.begin(), s.end(), s.begin(), toupper); return s; } const std::string & January() { static std::string jan("January"); return jan; } int main(void) { std::vector<std::string> authors { "A", "B", "C" }; Book b1("Book1", authors, 1, "Jan", 2012); // old c-tor size_t year = 2012 Book b2("Book2", { "A", "B", "C" }, 1, "Jan", year); // old c-tor std::string month = "Mar"; Book b3("Book3", { "Author" }, 1, toUpper(month), 2012); // old c-tor Book b4("Book4", { "Author" }, 1, January(), 2012); // old c-tor std::string book = "Book"; Book b5(std::move(book), std::move(authors), 1, std::move(month), year); // old-ctor Book b6("Book", { "Author" }, 1, "Jan", 2012); // new c-tor! }
It may come as a surprise that in all but one case above, the old constructor is called. Only in the last case, the new-ctor is called, which makes the minimum number of copies. So what's the gotcha?
As it turns out, the Book constructors are stopping the compiler from doing a better job. The first constructor takes all the parameters as const reference and the second one takes all the parameters as rvalue reference. Unless and until all the the parameters passed to the constructor are temporary objects (rvalues) or literals, the second constructor does not kick in. This implies lost optimization opportunities for parameters that are temporary (so can be moved) but won't be moved because some other parameter botched the soup. These two constructors give you very limited options: all-or-nothing.
In case of b1, authors is an lvalue and it does not bind to an rvalue ref.
In case of b2, year is an lvalue and does not bind to an rvalue ref.
In case of b3, toUpper returns a string reference; Does no good.
In case of b4, January returns a const string reference; same story!
In case of b5, year is an lvalue that prevents calling the second constructor although programmer tries to explicitly move all the string and vector parameters. In reality, the actual moves do not happen and if the remaining program depends on the moves being successful may not be very happy.
Only in case of b6, all actual parameters are rvalues or literals. Therefore, the "all-rvalue-ref" constructor is called. Note that temporary string objects will be implicitly created where string literals are used.
So lets fix it. What we really need is just one constructor that accepts all the parameters by value. As a side-effect, the overall code is much simpler.
class Book { public: Book(std::string title, std::vector<std::string> authors, size_t pub_day std::string pub_month, size_t pub_year) : _title (std::move(title)), _authors (std::move(authors)), _pub_day (pub_day), _pub_month(std::move(pub_month)), _pub_year (pub_year) {} // .... // .... };
That's it! This is our the constructor that works optimally. All strings, vectors, integral types are passed by value. Within the constructor, the pass-by-value parameters that are on the stack-frame must be moved to the respective data members. The reason is that the lifetime of pass-by-value parameters is anyways limited to the lifetime of the constructor call itself. We want to tie their lifetime to the object and not the constructor.
This constructor does no more deep copies of than necessary. And that number really depends on how it is called. Due to pass-by-value semantics, each object individually is an opportunity for the compiler to perform a move when a temporary is involved. Lets revisit our b1 to b4 objects.
In case of b1, except for authors all other parameters are copied and moved once.
In case of b2, all strings and vectors are copied and moved once. That's what matters.
In case of b3, toUpper returns an string reference; everything else copied and moved only once.
In case of b4, January returns a const string reference; everything else copied and moved once.
In case of b5, strings are vector are moved as expected and in fact the b5 object the local parameters are created using move-constructor and moved again into the data member. Hence the object is created without any deep copies (like zero-copy).
Using pass-by-value also opens other opportunities to eliminate temporaries when functions return by value and are used as actual parameters. However, I won't discuss that in detail here.
Finally, I want to conclude saying that this whole thing assumes that a move is much cheaper than a deep-copy. This is generally true for std::vector and std::string where memory is allocated dynamically. For small strings however small-string-optimization may make copy and move practically equivalent.
Comments
Am I missing something?
Here is a section of Dave Abrahams’s article: Want Speed? Pass by Value.
Although the compiler is normally required to make a copy when a function parameter is passed by value (so modifications to the parameter inside the function can’t affect the caller), it is allowed to elide the copy, and simply use the source object itself, when the source is an rvalue.
That's the cool thing about move semantics, it kinda cleans up the code in some contexts as well as providing the performance benefits.
So it's super easy to implement full move semantics for your types if you're already using idiomatic code. And move elision is possible too, so sometimes even the move constructor won't be called if you do it that way.
@AlfC: As far as I know, std::string and std:vector are implemented "correctly" because they are "standard".
However if we pass by a reference, only a reference is copied to the constructor, and than, the member object is copied from the referenced object. So reference pass + deep copy.
Is really the first method more effective? Or am I wrong about what is happening?
I think an important piece missing from the description given is that most popular compilers will do copy elision on rvalues passed into functions. This means that for rvalues passed to the constructor, only a single move occurs vs 1 copy and 1 move. It is possible the compiler doesn't elide the copy when an rvalue is passed, and assuming the rvalue is movable, you'd get 2 moves instead of 1, which should still typically be cheaper than a copy operation.
As for passing lvalues by reference to the constructor. It's going to need to make a copy anyway, so you're paying for the cost of 1 move and 1 copy vs a single copy if passed by reference. Assuming move is cheap the cost of the extra move when passing lvalues shouldn't be a of much concern.
If you really want to have the most efficient code, you could override your constructor to take every permutation of rvalue refs and const lvalues, but that can quickly become terribly impractical and a maintenance nightmare.
If you are using a third party library which is not move enabled, yet there is some more efficient move-like operation you can do with it, you have to turn to other solutions (such as overloading on rvalue ref, or using a wrapper class that adds move operation).
_authors (std::move(authors)),
Isn't it obvious here for the compiler that this is a temporary and move constructor should automatically kick in without having to specify manually?
In non-template code, if all the parameter types provide an efficient move constructor, the proposed technique in the post is a safe bet. You see at least two exceptions. First, non-template code: So what's different in template code? Templates open up more possibilities, such as perfect forwarding, which depends on using pass-by-rvalue-reference as opposed to pass-by-value. Compiler will figure out the best way passing the parameter. However, there is a gotcha and its solution rather requires pretty deep knowledge of language mechanics. So much wizardry is packed in it that it is counter productive IMHO. Please see this for more detail: http://codesynthesis.com/~boris/blog/2012/03/14/rvalue-reference-pitfalls-update
Second, efficient move ctor: Not all types have efficient move c-tor. for example, std::array, std::complex, etc. In such cases pass by const reference is probably the best idea.
Another cool thing is if you're implementing the copy-and-swap idiom. If you implement the assignment operator by taking the rhs parameter by value (which you already should do, to exploit any potential copy elision), you can get a move assignment operator for free simply by defining a move constructor. Then the one assignment operator will work for both move and copy assignment purposes, rather than needing two different assignment operators.
I'm not certain that it is so simple. You want your move assignment to be declared "noexcept" (so that std::swap() will be "noexcept") and you can do that if you create your own move assignment, but your copy assignment probably can't be noexcept (for example if have a std::string data member).
http://blog.panqnik.pl/dev/co-variant-and-virtual-constructor-in-c/
A size_t fits in a register and can be copied for free, whereas passing it by reference will force it onto the stack and put an extra level of indirection into the access.