Search code examples
c++c++11lvaluervalue

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.


Solution

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