I need to store universal references inside a class (I am sure the referenced values will outlive the class). Is there a canonical way of doing so?
Here is a minimal example of what I have come up with. It seems to work, but I'm not sure if I got it right.
template <typename F, typename X>
struct binder
{
template <typename G, typename Y>
binder(G&& g, Y&& y) : f(std::forward<G>(g)), x(std::forward<Y>(y)) {}
void operator()() { f(std::forward<X>(x)); }
F&& f;
X&& x;
};
template <typename F, typename X>
binder<F&&, X&&> bind(F&& f, X&& x)
{
return binder<F&&, X&&>(std::forward<F>(f), std::forward<X>(x));
}
void test()
{
int i = 1;
const int j = 2;
auto f = [](int){};
bind(f, i)(); // X&& is int&
bind(f, j)(); // X&& is const int&
bind(f, 3)(); // X&& is int&&
}
Is my reasoning correct or will this lead to subtle bugs? Also, is there a better (i.e., more concise) way to write the constructor? binder(F&& f, X&& x)
will not work, since those are r-value references, thus disallowing binder(f, i)
.
You can't "store a universal reference" because there's no such thing, there are only rvalue references and lvalue references. "Universal reference" is Scott Meyers's convenient term to describe a syntax feature, but it's not part of the type system.
To look at specific details of the code:
Here you're instantiating
binder
with reference types as the template arguments, so in the class definition there is no need to declare the members as rvalue-references, because they already are reference types (either lvalue or rvalue as deduced bybind
). This means you've always got more&&
tokens than needed, which are redundant and disappear due to reference collapsing.If you're sure
binder
will always be instantiated by yourbind
function (and so always be instantiated with reference types) then you could define it like this:In this version the types
F
andX
are reference types, so it's redundant to useF&&
andX&&
because they're either already lvalue references (so the&&
does nothing) or they're rvalue references (so the&&
does nothing in this case too!)Alternatively, you could keep
binder
as you have it and changebind
to:Now you instantiate
binder
with either an lvalue reference type or an object (i.e. non-reference) type, then insidebinder
you declare members with the additional&&
so they are either lvalue reference types or rvalue reference types.Furthermore, if you think about it, you don't need to store rvalue reference members. It's perfectly fine to store the objects by lvalue references, all that matters is that you forward them correctly as lvalues or rvalues in the
operator()
function. So the class members could be justF&
andX&
(or in the case where you always instantiate the type with reference arguments anyway,F
andX
)So I would simplify the code to this:
This version preserves the desired type in the template parameters
F
andX
and uses the right type in thestd::forward<X>(x)
expression, which is the only place where it's needed.Finally, I find it more accurate and more helpful to think in terms of the deduced type, not just the (collapsed) reference type:
binder(F&& f, X&& x)
works fine.F
andX
are reference types (they are the types given in the template instantiationbinder<F&&, X&&>
), and sof
andx
become universal reverences following the usual reference collapsing rules.Other than that, the code looks fine (as long as you really can ensure that the referenced variables outlive the binder).