Pass lvalue to rvalue

谁说我不能喝 提交于 2021-01-21 07:16:48

问题


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.


回答1:


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.




回答2:


It seems to me that enqueue(const&) and enqueue(&&) are just a special case of enqueue_emplace. Any good C++11-like container has these three functions and the first two are a special case of the third.

void enqueue(const T& item) { enqueue_emplace(item); }
void enqueue(T&& item)      { enqueue_emplace(std::move(item)); }

template <typename... Args>
void enqueue_emplace(Args&&... args)
{
    std::unique_lock<std::mutex> lock(m);
    this->emplace(std::forward<Args>(args)...); // queue already has emplace
    this->data_available = true;
    cv.notify_one();
}

This is a simple but efficient solution and it should behave the same as your original interface. It is also superior to the template approach because you can enqueue a initializer list.


Old post: Just do good old pass by value:

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

enqueue "owns" it's parameter and wants to move it into the queue. Pass by value is the right concept to say this.

It does exactly one copy and one move. Unfortunately this may be slow for Ts that don't have an optimized move constructor. I think for this reasons, the containers in the standard library always have two overloads. But on the other hand, a good compiler may optimize this away.




回答3:


Have you looked at std::forward? it might just do what you ask, if you throw in a little templating into your function...



来源:https://stackoverflow.com/questions/14667921/pass-lvalue-to-rvalue

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!