Suppose I want to pass a std::unique_ptr to a constructor, where the std::unique_ptr will be moved into a data member. The std::unique_ptr parameter thus acts as a sink. To the extent that we have enough experience with C++11 for wisdom about it to be conventional, said wisdom seems to be that the std::unique_ptr should be passed by value. In his GotW 91 solution, Herb Sutter argues for it. The High Integrity Coding Standard has it as a guideline. (It cites Herb's article as the source.) In his C++ Reference Guide, Danny Kalev argues for it. Many StackOverflow answers repeat this advice.
But recently Matthew Fioravante brought a StackOverflow question to my attention showing a problem resulting from declaring a std::unique_ptr by-value sink parameter, and later Matthew suggested that sink parameters of move-only types should be passed by rvalue reference. This is a very interesting idea.
Suppose you see this function signature:
void f(SomeType&& param);What does this tell you about param? The fact that it's an rvalue reference tells you that it's a candidate to be moved from, and the usual expectation is that it will be. In other words, it's a sink parameter. Note that this is completely independent of param's type. Even without knowing anything about SomeType, we can conclude that param is a sink parameter.
If SomeType happens to be std::unique_ptr, nothing changes: param is still a sink. There's no need for a special rule for std::unique_ptrs that tells us to pass them by value to indicate that they're sinks, because we already have a way to unambiguously say that: pass them by rvalue reference.
Going back to the idea of a constructor moving a std::unique_ptr into a data member, this is what the code looks like using pass by value:
class Widget { public: explicit Widget(std::unique_ptr ptr): p(std::move(ptr)) {} private: std::unique_ptr p; };Now consider this calling code:
std::unique_ptr up; Widget(std::move(up));What's the cost of getting up into p? Well, the parameter ptr has to be constructed, and the data member p does, too. Each costs a move construction, so the total cost is two move constructions (modulo optimizations).
Now consider the same thing using pass by rvalue reference:
class Widget { public: explicit Widget(std::unique_ptr&& ptr): p(std::move(ptr)) {} private: std::unique_ptr p; }; std::unique_ptr up; Widget(std::move(up));Here, only the data member p will be constructed, so the total cost is only one move construction.
Unless I'm overlooking something, passing sink parameters of type std::unique_ptr by value is inconsistent with our usual idiom for expressing the idea of a sink parameter (i.e., to pass by rvalue reference), and it's less efficient, too. My sense is that the conventional wisdom regarding sink parameters of type std::unique_ptr is all messed up.
Which leads to the question: how did it get messed up? I believe what happened was that people noticed that for maximal efficiency when passing lvalues and rvalues of a particular type that needed to be copied inside the function, you needed to either overload on lvalue references and rvalue references, or you needed to pass by universal reference. Both approaches have problems (overloading doesn't scale to multiple parameters, and universal references suffer from the shortcomings of perfect forwarding, lousy error messages, and sometimes being too greedy). For cheap-to-move types, people found, you can use pass by value with only a modest efficiency loss, and the conventional wisdom, in large part based on a David Abrahams' blog post, "Want Speed? Pass by value", came to embrace that idea.
The thing is, for move-only types like std::unique_ptr, you don't need to worry about dealing with lvalues, because lvalues get copied, and move-only types aren't copyable. So there's no need to overload for lvalues and rvalues, hence no scalability problem for multiple parameters. Which means that the motivations for replacing pass by reference--which is what the conventional wisdom from C++98 always dictated--with pass by value don't exist for move-only types.
My feeling is that Matthew Fioravante may well have hit the nail on the head here: there is no reason to use by-value parameters to express "sinkness" for move-only types. Instead, the usual rule of passing sink parameters by rvalue reference should apply.
The special case of considering the use of pass by value for always-copied parameters really only applies to types that are both copyable and movable, and only in situations where overloading and the use of a universal reference is not desired.
What do you think? Is there ever a time where move-only types should be passed by value?
Scott