Before I answer your questions, one thing you seem to be getting wrong: taking by value in C++11 does not always mean copying. If an rvalue is passed, that will be moved (provided a viable move constructor exists) rather than being copied. And std::string
does have a move constructor.
Unlike in C++03, in C++11 it is often idiomatic to take parameters by value, for the reasons I am going to explain below. Also see this Q&A on StackOverflow for a more general set of guidelines on how to accept parameters.
Why aren't we taking an rvalue-reference to str
?
Because that would make it impossible to pass lvalues, such as in:
std::string s = "Hello";
S obj(s); // s is an lvalue, this won't compile!
If S
only had a constructor that accepts rvalues, the above would not compile.
Won't a copy be expensive, especially given something like std::string
?
If you pass an rvalue, that will be moved into str
, and that will eventually be moved into data
. No copying will be performed. If you pass an lvalue, on the other hand, that lvalue will be copied into str
, and then moved into data
.
So to sum it up, two moves for rvalues, one copy and one move for lvalues.
What would be the reason for the author to decide to make a copy then a move?
First of all, as I mentioned above, the first one is not always a copy; and this said, the answer is: "Because it is efficient (moves of std::string
objects are cheap) and simple".
Under the assumption that moves are cheap (ignoring SSO here), they can be practically disregarded when considering the overall efficiency of this design. If we do so, we have one copy for lvalues (as we would have if we accepted an lvalue reference to const
) and no copies for rvalues (while we would still have a copy if we accepted an lvalue reference to const
).
This means that taking by value is as good as taking by lvalue reference to const
when lvalues are provided, and better when rvalues are provided.
P.S.: To provide some context, I believe this is the Q&A the OP is referring to.
Best Answer
Correction to the question
The way to implement Copy & Move has to be as @Raxvan pointed out:
but without the
std::move
asT(other)
already is an rvalue and clang will emit a warning about pessimisation when usingstd::move
here.Summary
When a move assignment operator exists, the difference between Copy & Swap and Copy & Move is dependent on whether the user is using a
swap
method which has better exception safety than the move assignment. For the standardstd::swap
the exception safety is identical between Copy & Swap and Copy & Move. I believe that most of the time, it will be the case thatswap
and the move assignment will have the same exception safety (but not always).Implementing Copy & Move has a risk where if the move assignment operator isn't present or has the wrong signature, the copy assignment operator will reduce to infinite recursion. However at least clang warns about this and by passing
-Werror=infinite-recursion
to the compiler this fear can be removed, which quite frankly is beyond me why that is not an error by default, but I digress.Motivation
I have done some testing and a lot of head scratching and here is what I have found out:
If you have a move assignment operator, the "proper" way of doing Copy & Swap won't work due to the call to
operator=(T)
being ambiguous withoperator=(T&&)
. As @Raxvan pointed out, you need to do the copy construction inside of the body of the copy assignment operator. This is considered inferior as it prevents the compiler from performing copy elision when the operator is called with an rvalue. However the cases where copy elision would have applied are handled by the move assignment now so that point is moot.We have to compare:
to:
If the user isn't using a custom
swap
, then the templatedstd::swap(a,b)
is used. Which essentially does this:Which means that the exception safety of Copy & Swap is the same exception safety as the weaker of move construction and move assignment. If the user is using a custom swap, then of course the exception safety is dictated by that swap function.
In the Copy & Move, the exception safety is dictated entirely by the move assignment operator.
I believe that looking at performance here is kind of moot as compiler optimizations will likely make there be no difference in most cases. But I'll remark on it anyway the copy and swap performs a copy construction, a move construction and two move assignments, compared to Copy & Move which does a copy construction and only one move assignment. Although I'm kind of expecting the compiler to crank out the same machine code in most cases, of course depending on T.
Addendum: The code I used