I want an object which can wrap a value symantic type and pretend that it is a reference.
Something like this:
int a = 20;
std::list<Wrap<int>> l1;
l1.push_back(a);
std::list<Wrap<int>> l2;
l2.push_back(a);
l2.front() = 10;
cout << l1.front() << endl; // output should be 10
While writing this question it occured to me that a shared_ptr
might be what I want. However I am not sure if a pointer symantic object is what I am looking for. Perhaps there is no alternative in the standard library?
std::shared_ptr<int> a = std::make_shared(10);
std::list<std::shared_ptr<int>> l1;
l1.push_back(a);
std::list<std::shared_ptr<int>> l2;
l2.push_back(a);
*(l2.front()) = 20; // not exactly what I wanted to write
cout << *l1.front() << endl; // prints 10
I found std::reference_wrapper
but this appears to be the opposite of what I want. It appears to permit a reference type to be used like a value type which is the reverse of what I wanted.
Any thoughts?
CodePudding user response:
Proxy Object
As far as I know, there isn’t such a wrapper in the Standard Library. Fortunately though, you can implement one yourself rather easily.
How to start? Well, we’d like our Wrap<T>
to be possibly indistinguishable from a T&
. To achieve this, we can implement it as a proxy object. So, we’ll be making a class template storing a T& ref
and implement operations one by one:
template<typename T>
struct Wrap
{
private:
T& ref;
public:
// ???
};
Construction
Let’s start with constructors. If we want Wrap<T>
to behave like a T&
it should be constructible from the same things that T&
is constructible. Well then, what is a reference x
constructible from?
A non-const
lvalue reference like T& x = ...;
needs to be constructed from an lvalue of type T
. This means that it can be constructed from:
- an object of type
T
- an object of a type derived from
T
- a different lvalue reference to
T
- an object convertible to
T&
- a function call returning
T&
A const
lvalue reference like const T& x = ...;
can also be constructed from a braced-initializer-list or materialized from a temporary, however if we wanted to implement that, then we’d need to actually store a T
inside of our class. As such, let’s focus on a non-const
reference.
First, let’s implement a constructor from T&
which will cover all of the cases shown above:
constexpr Wrap(T& t) noexcept :
ref{t}
{
}
The constructor is constexpr
and noexcept
because it can be. It is not explicit
because we want to be able to use Wrap
as transparently as possible.
We need to remember though, that a reference needs to be initializable from a different reference. Because we want our type to behave just like a builtin reference, we need to be able to
- initialize a
Wrap
from aT&
(already implemented above) - initialize a
Wrap
from a differentWrap
- initialize a
T&
from aWrap
To meet these criteria, we’ll need to implement a conversion operator to T&
. If Wrap<T>
will be implicitly convertible to T&
, then assigning it to a builtin reference will work.
Actually, this:
int x;
Wrap w = x;
Wrap w2 = w;
will also work (ie. w2
will be Wrap<int>
rather than a Wrap<Wrap<int>>
) because the conversion operator takes precedence.
The implementation of the conversion operator looks like this:
constexpr operator T&() const noexcept
{
return ref;
}
Note that it is constexpr
, noexcept
and const
, but not explicit
(just like the constructor).
Miscelaneous operations
Now that we can construct our custom reference wrapper, we’d also like to be able to use it. The natural question is “what can you do with a reference”? Well, builtin references aren’t objects but merely aliases to existing objects. This means that taking the address of the reference actually returns the address of the referent or that you can access members through a reference.
All these things could be implemented, for example by overloading the arrow operator operator->
or the address-of operator operator&
. For simplicity though, I will omit implementing these operations and focus only on the most important one: assignment.
Assignment
Firstly, we’d like to be able to assign a Wrap<T>
to a T&
or a T
or basically anything that can be assigned a T
. Fortunately, we already got that covered by having implemented the conversion operator.
Now we only need to implement assignment to Wrap<T>
. We could be tempted to just write the operator like this:
constexpr T& operator=(const T& t)
{
ref = t;
return ref;
}
constexpr T& operator=(T&& t) noexcept
{
ref = std::move(t);
return ref;
}
Seems fine, right? We have a copy assignment operator and a noexcept move assignment operator. We return a reference as per custom.
Well, the problem is that this implementation is incomplete. The thing is that we don’t check
- is
T
copy-assignable- if it is, then is it nothrow-copy-assignable
- is
T
move-assignable- if it is, then is it nothrow-move-assignable
- what is the return type of
T
’s assignment operator (it could be the customaryT&
, but it could also theoretically be anything else, likevoid
) - does
T
have any other assignment operators
This is a lot of cases to cover. Fortunately, we can solve this all by making our assignment operator a template.
Let’s say that the operator will take an object of arbitrary type U
as its argument. This will cover both the copy and move assignment operators and any other potential assignment operators. Then, let’s say that the return type of the function will be auto
, to let the compiler deduce it. This gives us the following implementation:
template <typename U>
constexpr auto operator=(U u)
{
return (ref = u);
}
Unfortunately though, this implementation is still not complete.
- We don’t know if the assignment is
noexcept
- We don’t distinguish copy assignment and move assignemnt and could potentially be making unnecessary copies.
- Is the return type (
auto
) really correct?
To solve the first issue we can use a conditional noexcept
. To check if the assignment operator is noexcept
we can either use the type trait std::is_nothrow_assignable_v
or the noexcept
operator. I think that using the noexcept
operator is both shorter and less error-prone, so let’s use that:
template <typename U>
constexpr auto operator=(U u) noexcept(noexcept(ref = u))
{
return (ref = u);
}
To solve the issue of distinguishing copies and moves, instead of taking a U u
, we can take a forwarding reference U&& u
, to let the compiler deal with all of this. We also need to remember about using std::forward
:
template <typename U>
constexpr auto operator=(U&& u) noexcept(noexcept(ref = std::forward<U>(u)))
{
return (ref = std::forward<U>(u));
}
There is a bit of code duplication, but, unfortunately, it is inevitable, unless we’d use std::is_nothrow_assignable_v
instead.
Finally, is the return type correct? Well, no. Because C is C , parentheses around the returned value actually change its type (ie. return(x);
is different from return x;
). To return the correct type, we’ll actually also need to apply perfect forwarding to the returned type as well. We can do this by either using a trailing return type or a decltype(auto)
return type. I will use decltype(auto)
as it’s shorter and avoids duplicating the function body yet again:
template <typename U>
constexpr decltype(auto) operator=(U&& u) noexcept(noexcept(ref = std::forward<U>(u)))
{
return ref = std::forward<U>(u);
}
Conclusion
Now, finally, we have a complete implementation. To sum things up, here it is all together (godbolt):
template<typename T>
struct Wrap
{
private:
T& ref;
public:
constexpr Wrap(T& t) noexcept :
ref{t}
{
}
constexpr operator T&() const noexcept
{
return ref;
}
template <typename U>
constexpr decltype(auto) operator=(U&& u) noexcept(noexcept(ref = std::forward<U>(u)))
{
return ref = std::forward<U>(u);
}
};
That was quite a bit of C type theory to get through to write these 21 lines. Oh, by the way, did I mention value categories...