Considering only objects that are movable but non-copyable (e.g., std::thread
or std::unique_ptr
), I want to transfer the ownership of the resource such an object contains by passing it as an argument to a constructor. I'm comparing two approaches: the constructor taking the object by value vs. by rvalue reference.
As an example with std::thread
, consider the following class Value
whose constructor takes an std::thread
by value:
#include <thread>
#include <utility>
struct Value {
Value(std::thread th): th_(std::move(th)) {}
std::thread th_;
};
The ownership is transferred from the argument object to the parameter object, th
, and finally to the data member object, th_
.
Consider a similar class, Reference
, whose constructor takes in this case an std::thread
by rvalue reference:
struct Reference {
Reference(std::thread&& th): th_(std::move(th)) {}
std::thread th_;
};
The ownership is, in this case, transferred from the argument directly to the data member object, th_
.
As far as I understand, in the passing-by-value case, both the parameter object and the data member object are move constructed, whereas, for the pass-by-reference case, only the data member is move constructed. To conclude, the latter approach seems to be better since it only requires one move operation, and it is, therefore, more efficient.
Is there, however, any reason to prefer the passing-by-value approach over the passing-by-reference one?
Passing by value is self-documenting in a way that passing by reference is not. It is immediately evident to the reader that:
(Similarly, it is difficult or impossible to make a mistake in the implementation that causes the above assumptions to fail to hold.)
Of course, ultimately the question of whether these benefits outweigh the costs of the extra move construction is an opinion-based one.
(*) Of course, this does not apply to the move that is performed internally by the constructor, i.e., from the parameter to the member. However, the point here is that the caller has control over what happens to their object: either it will be moved into the constructor parameter, or, in case of any failure at that step, the caller can do damage control (possibly to retain the value).