A Universal Reference Wrapper
This is a guest post by Ábel Surányi. Ábel is working as a software engineer in the IT security industry. He likes generic and functional programming, especially building abstractions by translating an idea to code in a way that the compiler can understand and catches errors during compilation. You can find Ábel on LinkedIn or on his GitHub.
Value categories are not a trivial topic in C++. Even experienced programmers struggle to get them right. This post is not about explaining them, but I attempt to implement a utility for making an expressive and safe way to reason about value categories and reduce the number of possible bugs caused by slipping over them.
Move and forward
I suppose that the reader knows about std::move
and std::forward
and how to use them.
Item 25: Use
std::move
on rvalue references,std::forward
on universal references.Scott Meyers – Effective modern C++
This rule works very well in most cases in generic code. There are only two problems with them: first is the cognitive burden: they work flawlessly only if they are used perfectly, the second one is sometimes they cannot handle some cases.
The main problem with std::move()
is that it unconditionally casts its argument to an rvalue reference. This is exactly the point of move, but this lead to interesting questions:
Consider an interface for a car service station (suppose that Car is a move-only type because copying would not make sense):
void inspect(const Car&); void repair(Car&); Car replace(Car&&);
When someone has a problem with her car and wants to find out, she will call inspect(mycar)
. The car mechanics cannot change anything on it, because it is taken by const reference. After that she can call repair(mycar)
to ask them to repair the car. The mechanics can change anything on it, but they are not supposed to replace it as a whole.
void repair(Car& car) { car = replace(std::move(car)); }
Are they allowed to do that? I would definitely start complaining if they gave me back another car (which possibly worse than mine). But they have an excuse: C++ allowed them to do that.
So can we write an expressive API where the caller (the owner of the car) has the right to replace her car by moving it, but it is not allowed for the mechanics who got the car just for fixing it only?
Add a level of abstraction
The root problem is that the programmer has to follow the value category of the variables and the compiler doesn’t help too much with that. What if we could teach the compiler somehow and use it to:
- generate correct code instead of redundant typing,
- fail the compilation instead of illegal runtime behaviour.
Introducing universal_wrapper
I will not go into the details of different value categories, but just make a simplification and split the cases in two groups: owners and references. Using this partition a type template can be defined, which knows what it holds.
struct owner_tag {}; struct reference_tag {}; template <typename Tag, typename T> struct universal_wrapper; template <typename T> struct universal_wrapper<owner_tag, T> { private: T value; }; template <typename T> struct universal_wrapper<reference_tag, T> { private: T& value; };
This is the basic idea: there is an owner wrapper and a reference wrapper. (Do not be confused: it has nothing to do with std::reference_wrapper
, although it can be considered as a generalization of that.)
There are a lot of things to do–mostly adding constraints–to make it safe and usable. We will see that there is a personal taste on those constraints: in this implementation I tended to a mostly strict version, which forces the users to be very explicit. It might put more work on them when a code is written, but it will be more readable and bring less surprises. Fortunately a new version can be added any time by defining a tag and a specialization.
Refining
First of all creating an owner where T is a reference must be illegal:
static_assert(!std::is_reference_v<T>, "T must not be a reference. Rather set the category!");
We can add the same assertion to the reference wrapper as well, since it adds the reference to it anyway. We can let T be const which is totally acceptable and should be supported.
Constructors
The owner wrapper’s constructor
constexpr universal_wrapper(T&& u) : value(std::move(u)) {}
The constructor should accept rvalue reference only. Optionally adding a constructor which accepts a const T&
and then copies can be considered, otherwise the copy has to be explicitly written on the caller side.
The reference wrapper’s constructor
explicit universal_wrapper(T& u) : value(u) {}
Reference specialization can be initialized from a reference (an object which already exists), but never from a temporary.
Accessing the value
The universal_wrapper
’s internal value member became private with reason, getters will be explicitly written and their implementation is essential from the perspective of the wrapper’s semantics.
This getter is the unsafe part of the interface, similarly for smart pointers’ .get()
function. It returns the underlying resource and the programmer can do bad or stupid things. For example calling delete ptr.get()
on a std::unique_ptr
is one of them. But these accessors are required to provide interoperability with the rest of the code. And the abused code is explicit which can easily be spotted on a code review. So those accessors should not be used for manipulating lifetime or value category, only for accessing the stored or referenced object.
There are three overloads for owning wrapper:
constexpr reference get() & { return value; } constexpr const_reference get() const & { return value; } constexpr value_type&& get() && { return std::move(value); }
Reference wrapper accessors:
constexpr reference get() { return t; } constexpr const_reference get() const { return t; }
Please note that for the reference wrapper there is no point to overload based on value category, since we do not want to move from the reference under any circumstances. If get()
is called on an rvalue reference it will select one of those overloads.
universal_wrapper<owner_tag, int> int_owner{...}; universal_wrapper<reference_tag, int> int_ref{...}; std::move(int_owner).get(); // int&& (moving) std::move(int_ref).get(); // int& (referencing)
The last two lines are syntactically the same, but semantically they make different things. There is a name for this kind of polymorphic behaviour: forwarding. It is a ‘forward’ from the perspective of the stored int based on the wrapper tag.
Fixing the car service API
So far this is the bare minimum implementation and now we can customize the behaviour:
- like transitions between owning and reference wrappers,
- handling mutability,
- implicit conversion to const,
- or enabling/disabling implicit copy,
- etc.
We will update the car service API, to see what needs to be improved.
template <typename T> using reference_to = universal_wrapper<reference_tag, T>; template <typename T> using owner = universal_wrapper<owner_tag, T>; void inspect(reference_to<const Car>); void repair(reference_to<Car>); owner<Car> replace(owner<Car>);
Universal wrappers should be used without any const or reference qualification, they keep this information in their type. My car is defined as the following way:
owner<Car> mycar{Car{...}}; inspect(mycar); // this does not compile inspect(mycar.ref()); repair(mycar.mutable_ref());
Here we need a ref()
and mutable_ref()
a function for the owning wrapper. Something like:
constexpr universal_wrapper<reference_tag, const T> ref() const & { return universal_wrapper<reference_tag, const T>{get()}; } constexpr universal_wrapper<reference_tag, const T> ref() & { return universal_wrapper<reference_tag, const T>{get()}; } constexpr universal_wrapper<reference_<wbr>tag, const T> ref() && = delete; constexpr universal_wrapper<reference_tag, T> mutable_ref() { return universal_wrapper<reference_tag, T>{get()}; } constexpr universal_wrapper<reference_tag, T> mutable_ref() && = delete;
By adding mutable_ref()
it is obvious on the caller side if the parameter is passed as a const or a mutable reference. The &&
-qualified overloads have to be deleted to prevent forming reference to a temporary object.
void repair(reference_to<Car> car) { replace(std::move(car)); // this does not compile anymore }
While on the top level replacing is possible only with the owner’s permission:
mycar = replace(std::move(mycar));
Out-of-line lambda
Previously on Fluent C++ we had a great post about out of line lambdas.
template<typename Function> class OutOfLineLambda { public: explicit OutOfLineLambda(Function function) : function_(function){} template<typename Context> auto operator()(Context& context) const { return [&context, this](auto&&... objects) { return function_(context, std::forward<decltype(objects)>(objects)...); }; } template<typename Context> auto operator()(Context&& context) const { return [context = std::move(context), this](auto&&... objects) { return function_(context, std::forward<decltype(objects)>(objects)...); }; } private: Function function_; };
While this code works perfectly, the question arises: do we really need two overloads?
It seems very straightforward: do not separate the lvalue and rvalue branches, just take context as a universal reference and forward it into the lambda:
template<typename Context> auto operator()(Context&& context) const { return [context = std::forward<Context>(context), this] (auto&&... objects) { return function_(context, std::forward<decltype(objects)>(objects)...); }; }
There is only one problem remaining: the lambda capture. It still captures by value (or by-copy as the standard refers to it). So the forward will decide to call context’s copy constructor or move constructor, but it won’t be captured by reference in either way. This problem can remain unnoticed if Context
is relatively cheap-to-copy and/or cheap-to-move. But suddenly fails to compile if a move-only type is passed by reference, because it cannot be copied in the lambda capture.
From that aspect it seems reasonable to have two overloads, one takes context by value and the other one takes &context
by reference.
This is when universal_wrapper
comes into the picture: we have a type which encodes this information in its type, so we can outsource the lambda capture problem to it. So update OutOfLineLambda
using universal_wrapper
:
template<typename Function> class OutOfLineLambda { public: explicit OutOfLineLambda(Function function) : function_(function) {} template<typename Context> auto operator()(Context&& context) const { return [wrapper = make_universal_wrapper(std::forward<Context>(context)), this] (auto&&... objects) { return function_(wrapper.get(), std::forward<decltype(objects)>(objects)...); }; } private: Function function_; };
make_universal_wrapper
will be our magic wand, which creates the proper universal_wrapper
specialization: owner for rvalues or reference to lvalues. Here is the last point where we need to type std::forward<Context>
to leverage the safety and convenience of universal_wrapper
that we achieved so far.
make_universal_wrapper
can be implemented in the following way:
namespace detail { template <typename T> struct ownership_tag : std::conditional< std::is_lvalue_reference_v<T>, reference_tag, owner_tag> {}; template <typename T> struct infer_universal_wrapper { using tag_type = typename ownership_tag<T>::type; using value_type = std::remove_reference_t<T>; using type = universal_wrapper<tag_type, value_type>; }; template <typename T> using infer_universal_wrapper_t = typename infer_universal_wrapper<T>::type; } template <typename T> constexpr auto make_universal_wrapper(T&& t) { return detail::infer_universal_wrapper_t<T>(std::forward<T>(t)); }
The main point here is to decide what is the tag, after that any kind of reference is peeled, since it would be refused by the universal_wrapper
anyway.
Conclusion
In my experience universal_wrapper
is especially useful for cases when a universal reference needs to be stored for later use like capturing in a lambda.
As it was presented by the car service station example, using universal_wrapper
specializations for public interface design can result in very expressive and robust code, however I am really interested in your opinion. Would you see this work in a real project or would it cause too much cognitive burden to the programmer compared to the advantages it provides, like improved safety and expressiveness?
Final thought
I started with a theoretical question, so I finish with another one: How should repair be implemented?
What I actually expected to do just replace the broken part on my car:
void repair(Car& car) { if (broken(car.gearbox)) { car.gearbox = replace_gearbox(std::move(car.gearbox)); } ... }
So the mechanic is not allowed to replace the car as a whole, but he can replace literally every part of it by moving them. If moving from a reference parameter is not acceptable, why is moving its members allowed? Who is the owner of these parts and how to express this ownership in code?
You will also like
- Understanding lvalues, rvalues and their references
- Out-of-line Lambdas
- Making code expressive with lambdas
- How to Get the “Table of Contents” Of a Long Function
- Smart Output Iterators >>= become(Pipes)
Share this post!