I am somewhat surprised that this is not answered very clearly and explicitly here, nor on any place I easily stumbled upon. While I'm pretty new to this stuff, I think the following can be said.
The situation is a calling function that builds a unique_ptr<T>
value (possibly by casting the result from a call to new
), and wants to pass it to some function that will take ownership of the object pointed to (by storing it in a data structure for instance, as happens here into a vector
). To indicate that ownership has been obtained by the caller, and it is ready to relinquish it, passing a unique_ptr<T>
value is in place. Ther are as far as I can see three reasonable modes of passing such a value.
- Passing by value, as in
add(unique_ptr<B> b)
in the question.
- Passing by non-
const
lvalue reference, as in add(unique_ptr<B>& b)
- Passing by rvalue reference, as in
add(unique_ptr<B>&& b)
Passing by const
lvalue reference would not be reasonable, since it does not allow the called function to take ownership (and const
rvalue reference would be even more silly than that; I'm not even sure it is allowed).
As far as valid code goes, options 1 and 3 are almost equivalent: they force the caller to write an rvalue as argument to the call, possibly by wrapping a variable in a call to std::move
(if the argument is already an rvalue, i.e., unnamed as in a cast from the result of new
, this is not necessary). In option 2 however, passing an rvalue (possibly from std::move
) is not allowed, and the function must be called with a named unique_ptr<T>
variable (when passing a cast from new
, one has to assign to a variable first).
When std::move
is indeed used, the variable holding the unique_ptr<T>
value in the caller is conceptually dereferenced (converted to rvalue, respectively cast to rvalue reference), and ownership is given up at this point. In option 1. the dereferencing is real, and the value is moved to a temporary that is passed to the called function (if the calles function would inspect the variable in the caller, it would find it hold a null pointer already). Ownership has been transferred, and there is no way the caller could decide to not accept it (doing nothing with the argument causes the pointed-to value to be destroyed at function exit; calling the release
method on the argument would prevent this, but would just result in a memory leak). Surprisingly, options 2. and 3. are semantically equivalent during the function call, although they require different syntax for the caller. If the called function would pass the argument to another function taking an rvalue (such as the push_back
method), std::move
must be inserted in both cases, which will transfer ownership at that point. Should the called function forget to do anything with the argument, then the caller will find himself still owning the object if holding a name for it (as is obligatory in option 2); this in spite of that fact that in case 3, since the function prototype asked the caller to agree to the release of ownership (by either calling std::move
or supplying a temporary). In summary the methods do
- Forces caller to give up ownership, and be sure to actually claim it.
- Force caller to possess ownership, and be prepared (by supplying a non
const
reference) to give it up; however this is not explicit (no call of std::move
required or even allowed), nor is taking away ownership assured. I would consider this method rather unclear in its intention, unless it is explicitly intended that taking ownership or not is at discretion of the called function (some use can be imagined, but callers need to be aware)
- Forces caller to explicitly indicate giving up ownership, as in 1. (but actual transfer of ownership is delayed until after the moment of function call).
Option 3 is fairly clear in its intention; provided ownership is actually taken, it is for me the best solution. It is slightly more efficient than 1 in that no pointer values are moved to temporaries (the calls to std::move
are in fact just casts and cost nothing); this might be especially relevant if the pointer is handed through several intermediate functions before its contents is actually being moved.
Here is some code to experiment with.
class B
{
unsigned long val;
public:
B(const unsigned long& x) : val(x)
{ std::cout << "storing " << x << std::endl;}
~B() { std::cout << "dropping " << val << std::endl;}
};
typedef std::unique_ptr<B> B_ptr;
class A {
std::vector<B_ptr> vb;
public:
void add(B_ptr&& b)
{ vb.push_back(std::move(b)); } // or even better use emplace_back
};
void f() {
A a;
B_ptr b(new B(123)),c;
a.add(std::move(b));
std::cout << "---" <<std::endl;
a.add(B_ptr(new B(4567))); // unnamed argument does not need std::move
}
As written, output is
storing 123
---
storing 4567
dropping 123
dropping 4567
Note that values are destroyed in the ordered stored in the vector. Try changing the prototype of the method add
(adapting other code if necessary to make it compile), and whether or not it actually passes on its argument b
. Several permutations of the lines of output can be obtained.