I have implemented a type-erasing reference class that can be constructed from an l-value reference to any type. However, I have run into a bit of a dilemma as to whether or not to allow construction from an r-value.
There are two use cases I have encountered:
Constructing the reference as a local variable
int i = 42;
Reference ref1 = i; // This is allowed.
Reference ref2 = 42; // This should cause a compile error.
Constructing the reference as a function parameter
void func(Reference ref);
int i = 42;
func(i); // This is allowed.
func(42); // This should also be allowed.
Effectively I want to allow implicit construction of a Reference
instance from anything with a lifespan greater than the reference, but not from anything with a shorter lifespan.
Is there some way to accomplish this, i.e., to allow func(42)
but disallow ref = 42
? I am free to make any necessary changes to Reference
, but the signature of func
should remain the same.
What you want in general cannot be done.
Deleting construct-from T&&
is reasonable, but T&&
does not convert to T&
unless T
is const
. This doesn't permit the function argument case.
You could use a different type for the function argument version than the local variable version.
A more general option is to construct from a universal reference and optionally store a copy if your argument is an rvalue.
template<class T>
struct view_type_t {
std::optional<T> possible_copy;
T* ptr;
view_type_t(T& t):ptr(std::addressof(t)) {}
view_type_t(T&& t):
possible_copy(std::forward<T>(t)),
ptr(std::addressof(*possible_copy))
{}
view_type_t() = delete;
// calls view_type_t&& ctor, possibly elided
view_type_t(view_type_t const& src):
view_type_t(
src.possible_copy?
view_type_t(T(*src.possible_copy)):
view_type_t(*src.ptr)
)
{}
// this is a bit tricky. Forwarding ctors doesn't work here,
// it MIGHT work in C++17 due to guaranteed elision
view_type_t(view_type_t&& src) {
if (src.possible_copy) {
possible_copy.emplace(std::move(*src.possible_copy));
ptr = std::addressof(*possible_copy);
} else {
ptr = src.ptr;
}
}
view_type_t& operator=(view_type_t const& src); // todo
view_type_t& operator=(view_type_t&& src); // todo
};
this emulates reference lifetime extension, where temporary lifetime is extended by the reference. Such a Reference
behaves like a naked rvalue reference does in C++.
Now Reference ref2 = 42;
works, and it is now a reference a local copy of 42
.
I find this technique better than the alternative when making other kinds of view types, like backwards_range
, which keeps a copy of its source range if it is an rvalue and doesn't if it is an lvalue.
This permits:
for( auto&& x : backwards( std::vector<int>{1,2,3} ) )
to work. More generally, if the vector was created as a temporary returned from a function, we can call backwards on it and iterate directly; without creating the local copy, we run into lifetime problems as the lifetime extension of temporaries doesn't commute.
Of course you need a std::optional
replacement (such as boost::optional
) outside of C++17 for the above code to work.
The only way I can think of is to delete
a constructor, and use an overloaded function:
struct Reference
{
Reference(int& i){};
Reference(const int&& i) = delete;
};
Since an anonymous temporary cannot bind to a non-const
reference, Reference ref2 = 42;
will fail at compile time due to the deleted constructor. Construction from an int
variable is allowed though.
On the second point, compilation passes if you introduce the overload
void func(int ref)
{
func(Reference(ref));
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With