- edited description
`upcxx::reference_wrapper` for supporting move-only types in `upcxx::future`
As discussed in the 4/28/2020 meeting, the inability to use move semantics with upcxx::future
prevents using types that are MoveConstructible but not CopyConstructible, such as std::unique_ptr
. This limitation also prevents moving objects from futures in cases where copying an object requires an expensive deep copy and a move, if possible, would be more efficient. At the time of the meeting, none of us had a workable solution, as upcxx::future
is a handle that can have a reference count >1. Thinking more about this problem after the meeting, I came up with a upcxx::reference_wrapper
as a potential solution. This solution takes inspiration from std::reference_wrapper
, as std::bind
has a similar issue in that multiple invocations should behave the same except for when references are used explicitly.
template<typename T>
class reference_wrapper
{
public:
reference_wrapper() noexcept = default;
reference_wrapper(const reference_wrapper&) noexcept = default;
reference_wrapper(reference_wrapper&&) noexcept = default;
reference_wrapper(T&& t) noexcept(noexcept(T{std::declval<T&&>()}))
: ptr{std::make_shared<T>(std::move(t))}
{}
reference_wrapper(const T& t) noexcept(noexcept(T{std::declval<const T&>()}))
: ptr{std::make_shared<T>(t)}
{}
T& get() const { return *ptr }
T move() const noexcept(noexcept(T{std::declval<T&&>()})) {
return std::move(*ptr);
}
private:
std::shared_ptr<T> ptr;
public:
struct upcxx_serialization {
template<typename Writer>
static void serialize(Writer& writer, const reference_wrapper& o) {
writer.write(o.move());
}
template<typename Reader>
static reference_wrapper* deserialize(Reader& reader, void* storage) {
reference_wrapper* res = ::new(storage) reference_wrapper(reader.template read<T>());
return res;
}
};
};
We can also introduce a helper function, upcxx::move()
:
template<typename T>
inline reference_wrapper<typename std::remove_reference<T>::type> move(T&& t) noexcept(noexcept(T{std::declval<T&&>()})) {
return reference_wrapper<typename std::remove_reference<T>::type>(std::move(t));
}
The helpers upcxx::ref
and upcxx::cref
to mirror the STL are also worth considering but probably aren’t as useful.
This provides a simple workaround for the copyability problem. upcxx::move()
is called when returning from an RPC to move the type to the return buffer and upcxx::reference_wrapper<T>::move()
is used from the returned future to move the object off of the buffer:
moveonly_type res = upcxx::rpc(0, []() {
moveonly_type m{};
return upcxx::move(m); //move into return buffer
}).wait().move(); //move out of return buffer
This proposal is compatible with future chaining:
moveonly_type res = upcxx::rpc(0, []() {
moveonly_type m{};
return upcxx::move(m); //move into return buffer
}).then([](const upcxx::reference_wrapper<moveonly_type>& m) {
m.get().mutate(); //modifies the type in-place from an lvalue
return m;
}).wait().move(); //move out of return buffer
This essentially makes the type accessible as non-const lvalue or rvalue references.
As with any move, the programmer would be responsible for only moving once. The side effects and ordering of non-const lvalue usage have to also be kept in mind. This requires more care than having only const lvalue references, but due to the explicit sematics of using a wrapper, this doesn’t introduce pitfalls that one could stumble into unwittingly. Because .move()
must be called explicitly and reference_wrapper
isn’t implicitly convertible to an rvalue of the underlying type, this design helps avoid UB when moving a type out of the future
. As with std::reference_wrapper
and actual references, the constness of the wrapper/reference does not impact the constness of the referred-to object.
The main disadvantage of this technique is the additional allocation needed for the std::shared_ptr
within upcxx::reference_wrapper
. The alternative would be to have the upcxx::future
only contain references and wrappers, only indirectly pointing to the actual data. This is the technique I developed to avoid this problem entirely in my MPIRPC and support any type of object. A std::shared_ptr<data_storage<unwrapped_t<T>...>> data
would contain the actual data and only exist once. A std::tuple<wrapped_or_reference_t<T>...> refs
is what would be passed around as a copyable type in a future or . A future
created as a composite of other futures
would thus contain a std::vector<std::shared_ptr<data_storage_base>>
to keep the underlying data alive and a std::tuple
of references referring to data from these multiple locations. Because no actual data would be copied or moved, only references, the future could be copied or moved freely without touching the referred-to data. This technique even allows for types which are both non-copyable and non-movable to be used, as the type would be deserialized and constructed in-place in the data_storage
and could be kept alive by getting a copy of the std::shared_ptr
. The future
would never deal in the underlying types directly. This technique is highly flexible, but would require a significant architectural change.
The proposed upcxx::reference_wrapper
and upcxx::move()
would enable this functionality and optimization potential with just a few dozen lines of code. Although the original motivation was only for dealing with std::unique_ptr
, the wrapper is general purpose and is useful functionality not only for move-only types but also for optimizing code that uses types with expensive copy constructors. Thus, I propose it as a part of core UPC++ rather than part of extras.
The original name I came up with was upcxx::move_wrapper
. However, I realized it might be useful to retrieve lvalue references to the stored object in upcxx::future
chains. reference_wrapper
might not quite be the right name for it.
Comments (7)
-
reporter -
reporter - edited description
-
- changed milestone to 2022.3.0 release
Mass roll-over of open issues to next release milestone
-
- changed milestone to 2022.9.0 release
Mass roll-over of open issues to next release milestone
-
- changed milestone to 2023.3.0 release
Mass roll-over of open issues to next release milestone
-
- changed milestone to 2023.9.0 release
Mass roll-over of open issues to next release milestone
-
- removed milestone
Clear past Milestone for open issues
- Log in to comment