Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pass lvalue to rvalue

I made a small 'blocking queue' class. It irritates me that I have created redundant code for values passed into the enqueue member function.

Here are the two functions that do the same exact thing (except the rvalue uses std::move to move the rvalue into the actual queue collection), except handles lvalue and rvalue respectively:

    void enqueue(const T& item)
    {
        std::unique_lock<std::mutex> lock(m);
        this->push(item);
        this->data_available = true;
        cv.notify_one();
    }

    void enqueue(T&& item)
    {
        std::unique_lock<std::mutex> lock(m);
        this->push(std::move(item));
        this->data_available = true;
        cv.notify_one();
    }

My question is, is there a way to combine these two functions, without losing support for rvalue references.

like image 434
TheAJ Avatar asked Feb 02 '13 23:02

TheAJ


People also ask

Can you pass an lvalue to an rvalue reference?

In the example, the main function passes an rvalue to f . The body of f treats its named parameter as an lvalue. The call from f to g binds the parameter to an lvalue reference (the first overloaded version of g ). You can cast an lvalue to an rvalue reference.

What is pass lvalue reference?

Pass by reference allows us to pass arguments to a function without making copies of those arguments each time the function is called. Pass by reference allows us to change the value of an argument.

What is perfect forwarding in C++?

What is Perfect Forwarding. Perfect forwarding allows a template function that accepts a set of arguments to forward these arguments to another function whilst retaining the lvalue or rvalue nature of the original function arguments.

Can a function return an lvalue?

Functions can be declared to return a reference type. There are two reasons to make such a declaration: The information being returned is a large enough object that returning a reference is more efficient than returning a copy. The type of the function must be an l-value.


1 Answers

This is a classic example of the need to perfectly forward. Do this by templating the function (member template if this is a member function):

template <class U>
void enqueue(U&& item)
{
    std::unique_lock<std::mutex> lock(m);
    this->push(std::forward<U>(item));
    this->data_available = true;
    cv.notify_one();
}

Explanation: If you pass an lvalue T to enqueue, U will deduce to T&, and the forward will pass it along as an lvalue, and you'll get the copy behavior you want. If you pass an rvalue T to enqueue, U will deduce to T, and the forward will pass it along as an rvalue, and you'll get the move behavior you want.

This is more efficient than the "pass by value" approach in that you never make an unnecessary copy or move. The downside with respect to the "pass by value" approach is that the function accepts anything, even if it is wrong. You may or may not get cascaded errors down under the push. If this is a concern, you can enable_if enqueue to restrict what arguments it will instantiate with.

Update based on comment

Based on the comments below, this is what I understand things look like:

#include <queue>
#include <mutex>
#include <condition_variable>

template <class T>
class Mine
    : public std::queue<T>
{
    std::mutex m;
    std::condition_variable cv;
    bool data_available = false;
public:

    template <class U>
    void
    enqueue(U&& item)
    {
        std::unique_lock<std::mutex> lock(m);
        this->push(std::forward<U>(item));
        this->data_available = true;
        cv.notify_one();
    }
};

int
main()
{
    Mine<int> q;
    q.enqueue(1);
}

This is all good. But what if you try to enqueue a double instead:

q.enqueue(1.0);

This still works because the double is implicitly convertible to the int. But what if you didn't want it to work? Then you could restrict your enqueue like this:

template <class U>
typename std::enable_if
<
    std::is_same<typename std::decay<U>::type, T>::value
>::type
enqueue(U&& item)
{
    std::unique_lock<std::mutex> lock(m);
    this->push(std::forward<U>(item));
    this->data_available = true;
    cv.notify_one();
}

Now:

q.enqueue(1.0);

results in:

test.cpp:31:11: error: no matching member function for call to 'enqueue'
        q.enqueue(1.0);
        ~~^~~~~~~
test.cpp:16:13: note: candidate template ignored: disabled by 'enable_if' [with U = double]
            std::is_same<typename std::decay<U>::type, T>::value
            ^
1 error generated.

But q.enqueue(1); will still work fine. I.e. restricting your member template is a design decision you need to make. What U do you want enqueue to accept? There is no right or wrong answer. This is an engineering judgment. And several other tests are available that may be more appropriate (e.g. std::is_convertible, std::is_constructible, etc.). Maybe the right answer for your application is no constraint at all, as first prototyped above.

like image 56
Howard Hinnant Avatar answered Oct 02 '22 23:10

Howard Hinnant