Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to store universal references

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).

like image 576
marton78 Avatar asked Feb 07 '13 17:02

marton78


People also ask

What is a universal reference?

Universal reference was a term Scott Meyers coined to describe the concept of taking an rvalue reference to a cv-unqualified template parameter, which can then be deduced as either a value or an lvalue reference.

What is reference collapsing?

Reference collapsing is the mechanism that leads to universal references (which are really just rvalue references in situations where reference-collapsing takes place) sometimes resolving to lvalue references and sometimes to rvalue references.

What are rvalue references?

Rvalue references is a small technical extension to the C++ language. Rvalue references allow programmers to avoid logically unnecessary copying and to provide perfect forwarding functions. They are primarily meant to aid in the design of higer performance and more robust libraries.

What is forwarding reference in C++?

When t is a forwarding reference (a function argument that is declared as an rvalue reference to a cv-unqualified function template parameter), this overload forwards the argument to another function with the value category it had when passed to the calling function.


1 Answers

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:

template <typename F, typename X>
binder<F&&, X&&> bind(F&& f, X&& x)

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 by bind). 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 your bind function (and so always be instantiated with reference types) then you could define it like this:

template <typename F, typename X>
struct binder
{
    binder(F g, X y) : f(std::forward<F>(g)), x(std::forward<X>(y)) {}

    void operator()() { f(std::forward<X>(x)); }

    F f;
    X x;
};

In this version the types F and X are reference types, so it's redundant to use F&& and X&& 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 change bind to:

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));
}

Now you instantiate binder with either an lvalue reference type or an object (i.e. non-reference) type, then inside binder 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 just F& and X& (or in the case where you always instantiate the type with reference arguments anyway, F and X)

So I would simplify the code to this:

template <typename F, typename X>
struct binder
{
    binder(F& g, X& y) : f(g), x(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>(f, x);
}

This version preserves the desired type in the template parameters F and X and uses the right type in the std::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:

bind(f, i)();   // X is int&, X&& is int&
bind(f, j)();   // X is const int&, X&& is const int&
bind(f, 3)();   // X is int, X&& is int&&
like image 173
Jonathan Wakely Avatar answered Sep 23 '22 09:09

Jonathan Wakely