c++c++11parameter-passingmove-semanticsownership-semantics

Movable but non-copyable objects: passing by value vs by rvalue reference?


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?


Solution

  • Passing by value is self-documenting in a way that passing by reference is not. It is immediately evident to the reader that:

    1. the constructor will unconditionally take ownership of the resource that is represented by the move-only type;
    2. should the move construction fail by throwing an exception for whatever reason, it is the caller's responsibility to deal with it; (*)
    3. should the constructor exit by throwing an exception, the resource will be disposed of.

    (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).